Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
705 lines (562 sloc) 19.9 KB
#!/usr/bin/env python
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
# The Original Code is reddit.
# The Original Developer is the Initial Developer. The Initial Developer of
# the Original Code is reddit Inc.
# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit
# Inc. All Rights Reserved.
import inspect
import sys
import os.path
import re
import subprocess
import json
from pylons import app_globals as g
from pylons import tmpl_context as c
from r2.config.paths import get_built_statics_path
from r2.lib.permissions import ModeratorPermissionSet
from r2.lib.plugin import PluginLoader
from r2.lib.static import locate_static_file
from r2.lib.translation import (
script_tag = '<script type="text/javascript" src="{src}"></script>\n'
inline_script_tag = '<script type="text/javascript">{content}</script>'
class Uglify(object):
def compile(self, data, dest):
process = subprocess.Popen(
["/usr/bin/uglifyjs", "-nc"],
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, "uglifyjs")
class Source(object):
"""An abstract collection of JavaScript code."""
def get_source(self, **kwargs):
"""Return the full JavaScript source code."""
raise NotImplementedError
def use(self, **kwargs):
"""Return HTML to insert the JavaScript source inside a template."""
raise NotImplementedError
def dependencies(self):
raise NotImplementedError
def outputs(self):
raise NotImplementedError
class FileSource(Source):
"""A JavaScript source file on disk."""
def __init__(self, name): = name
def get_source(self, use_built_statics=False):
if use_built_statics:
# we are in the build system so we have already copied all files
# into the static build directory
built_statics_path = get_built_statics_path()
path = os.path.join(built_statics_path, "static", "js",
# we are in request so we need to check the pylons static_files
# path and the static paths for all plugins
path = locate_static_file(os.path.join("static", "js",
with open(path) as f:
def url(self, absolute=False, mangle_name=False):
from r2.lib.template_helpers import static
path = [g.static_path,]
if g.uncompressedJS:
path.insert(1, "js")
return static(os.path.join(*path), absolute, mangle_name)
def use(self, **kwargs):
return script_tag.format(src=self.url(**kwargs))
def dependencies(self):
built_statics_path = get_built_statics_path()
path = os.path.join(built_statics_path, "static", "js",
return [path]
class Module(Source):
"""A module of JS code consisting of a collection of sources."""
def __init__(self, name, *sources, **kwargs): = name
self.should_compile = kwargs.get('should_compile', True)
self.wrap = kwargs.get('wrap')
self.sources = []
sources = sources or (name,)
for source in sources:
if not isinstance(source, Source):
if 'prefix' in kwargs:
source = os.path.join(kwargs['prefix'], source)
source = FileSource(source)
def get_source(self, use_built_statics=False):
return ";".join(
for s in self.sources
def extend(self, module):
def destination_path(self):
built_statics_path = get_built_statics_path()
return os.path.join(built_statics_path, "static",
def build(self, minifier):
with open(self.destination_path, "w") as out:
source = self.get_source(use_built_statics=True)
if self.wrap:
source = self.wrap.format(content=source,
if self.should_compile:
print >> sys.stderr, "Compiling {0}...".format(,
minifier.compile(source, out)
print >> sys.stderr, "Concatenating {0}...".format(,
print >> sys.stderr, " done."
def url(self, absolute=False, mangle_name=True):
from r2.lib.template_helpers import static
if g.uncompressedJS:
return [source.url(absolute=absolute, mangle_name=mangle_name) for source in self.sources]
return static(, absolute=absolute, mangle_name=mangle_name)
def use(self, **kwargs):
if g.uncompressedJS:
return "".join(source.use(**kwargs) for source in self.sources)
return script_tag.format(src=self.url(**kwargs))
def dependencies(self):
deps = []
for source in self.sources:
return deps
def outputs(self):
return [self.destination_path]
class DataSource(Source):
"""A generated source consisting of wrapped JSON data."""
def __init__(self, wrap, data=None):
self.wrap = wrap = data
def get_content(self, **kw):
def get_source(self, use_built_statics=False):
content = self.get_content(use_built_statics=use_built_statics)
json_data = json.dumps(content)
return self.wrap.format(content=json_data) + "\n"
def use(self):
from r2.lib.filters import SC_OFF, SC_ON, websafe_json
escaped_json = websafe_json(self.get_source())
return (SC_OFF + inline_script_tag.format(content=escaped_json) +
SC_ON + "\n")
def dependencies(self):
return []
class PermissionsDataSource(DataSource):
"""DataSource for PermissionEditor configuration data."""
def __init__(self, permission_sets):
self.permission_sets = permission_sets
def _make_marked_json(cls, obj):
"""Return serialized psuedo-JSON with translation support.
Strings are marked for extraction with r.N_. Dictionaries are
serialized to JSON objects as normal.
if isinstance(obj, dict):
props = []
for key, value in obj.iteritems():
value_encoded = cls._make_marked_json(value)
props.append("%s: %s" % (key, value_encoded))
return "{%s}" % ",".join(props)
elif isinstance(obj, basestring):
return "r.N_(%s)" % json.dumps(obj)
raise ValueError, "unsupported type"
def get_source(self, **kw):
permission_set_info = {k: for k, v in
permissions = self._make_marked_json(permission_set_info)
return "r.permissions = _.extend(r.permissions || {}, %s)" % permissions
def dependencies(self):
dependencies = set(super(PermissionsDataSource, self).dependencies)
for permission_set in self.permission_sets.itervalues():
return list(dependencies)
class TemplateFileSource(DataSource, FileSource):
"""A JavaScript template file on disk."""
def __init__(self, name, wrap="r.templates.set({content})"):
DataSource.__init__(self, wrap)
FileSource.__init__(self, name) = name
def get_content(self, use_built_statics=False):
name, style = os.path.splitext(
if use_built_statics:
built_statics_path = get_built_statics_path()
path = os.path.join(built_statics_path, 'static', 'js',
path = locate_static_file(os.path.join('static', 'js',
with open(path) as f:
return [{
"name": name,
"style": style.lstrip('.'),
class LocaleSpecificSource(object):
def get_localized_source(self, lang):
raise NotImplementedError
class StringsSource(LocaleSpecificSource):
"""Translations sourced from a gettext catalog."""
def __init__(self, keys):
self.keys = keys
invalid_formatting_specifier_re = re.compile(r"(?<!%)%\w|(?<!%)%\(\w+\)[^s]")
def _check_formatting_specifiers(self, string):
if not isinstance(string, basestring):
raise ValueError("Invalid string formatting specifier: %r" % string)
def get_localized_source(self, lang):
catalog = get_catalog(lang)
# relies on pyx files, so it can't be imported at global scope
from r2.lib.utils import tup
data = {}
for key in self.keys:
key = tup(key)[0] # because the key for plurals is (sing, plur)
msg = catalog[key]
if not msg or not msg.string:
# jed expects to ignore the first value in the translations array
# so we'll just make it null
strings = tup(msg.string)
data[key] = [None] + list(strings)
return "r.i18n.addMessages(%s)" % json.dumps(data)
class PluralForms(LocaleSpecificSource):
def get_localized_source(self, lang):
catalog = get_catalog(lang)
return "r.i18n.setPluralForms('%s')" % catalog.plural_expr
class LocalizedModule(Module):
"""A module that generates localized code for each language.
Strings marked for translation with one of the functions in i18n.js (viz.
r._, r.P_, and r.N_) are extracted from the source and their translations
are built into the compiled source.
def __init__(self, *args, **kwargs):
self.localized_appendices = kwargs.pop("localized_appendices", [])
Module.__init__(self, *args, **kwargs)
for source in self.sources:
if isinstance(source, LocalizedModule):
def languagize_path(path, lang):
path_name, path_ext = os.path.splitext(path)
return path_name + "." + lang + path_ext
def build(self, minifier):, minifier)
with open(self.destination_path) as f:
reddit_source =
localized_appendices = self.localized_appendices
msgids = extract_javascript_msgids(reddit_source)
if msgids:
localized_appendices = localized_appendices + [StringsSource(msgids)]
print >> sys.stderr, "Creating language-specific files:"
for lang, unused in iter_langs():
lang_path = LocalizedModule.languagize_path(
self.destination_path, lang)
# make sure we're not rewriting a different mangled file
# via symlink
if os.path.islink(lang_path):
with open(lang_path, "w") as out:
print >> sys.stderr, " " + lang_path
for appendix in localized_appendices:
out.write(appendix.get_localized_source(lang) + ";")
def use(self, **kwargs):
from pylons.i18n import get_lang
from r2.lib.template_helpers import static
from r2.lib.filters import SC_OFF, SC_ON
if g.uncompressedJS:
if c.lang == "en" or c.lang not in g.all_languages:
# in this case, the msgids *are* the translated strings and we
# can save ourselves the pricey step of lexing the js source
return Module.use(self, **kwargs)
msgids = extract_javascript_msgids(Module.get_source(self))
localized_appendices = self.localized_appendices + [StringsSource(msgids)]
lines = [Module.use(self, **kwargs)]
for appendix in localized_appendices:
line = SC_OFF + inline_script_tag.format(
content=appendix.get_localized_source(c.lang)) + SC_ON
return "\n".join(lines)
langs = get_lang() or [g.lang]
url = LocalizedModule.languagize_path(, langs[0])
return script_tag.format(src=static(url), **kwargs)
def outputs(self):
for lang, unused in iter_langs():
yield LocalizedModule.languagize_path(self.destination_path, lang)
module = {}
catch_errors = "try {{ {content} }} catch (err) {{ r.sendError('Error running module', '{name}', ':', err.toString()) }}"
module["gtm-jail"] = Module("gtm-jail.js",
module["gtm"] = Module("gtm.js",
module["reddit-embed-base"] = Module("reddit-embed-base.js",
module["reddit-embed"] = Module("reddit-embed.js",
module["comment-embed"] = Module("comment-embed.js",
module["reddit-init-base"] = LocalizedModule("reddit-init-base.js",
module["reddit-init-legacy"] = LocalizedModule("reddit-init-legacy.js",
module["reddit-init"] = LocalizedModule("reddit-init.js",
module["reddit"] = LocalizedModule("reddit.js",
"moderator": ModeratorPermissionSet,
"moderator_invite": ModeratorPermissionSet,
module["modtools"] = Module("modtools.js",
# move into reddit module just after permissions when released
module["timeouts"] = Module("timeouts.js",
module["admin"] = Module("admin.js",
# include Backbone and timings so they are available early to render admin bar fast.
module["mobile"] = LocalizedModule("mobile.js",
module["policies"] = Module("policies.js",
module["sponsored"] = LocalizedModule("sponsored.js",
module["timeseries"] = Module("timeseries.js",
module["timeseries-ie"] = Module("timeseries-ie.js",
module["traffic"] = LocalizedModule("traffic.js",
module["qrcode"] = Module("qrcode.js",
module["highlight"] = Module("highlight.js",
module["less"] = Module('less.js',
# This needs to be separate module because we need it to load on old / bad
# browsers that choke on reddit.js
module["https-tester"] = Module("https-tester.js",
def src(*names, **kwargs):
sources = []
for name in names:
urls = module[name].url(**kwargs)
if isinstance(urls, str) or isinstance(urls, unicode):
for url in list(urls):
if isinstance(url, list):
return sources
def use(*names, **kwargs):
return "\n".join(module[name].use(**kwargs) for name in names)
def load_plugin_modules(plugins=None):
if not plugins:
plugins = PluginLoader()
for plugin in plugins:
commands = {}
def build_command(fn):
def wrapped(*args):
commands[fn.__name__] = wrapped
return wrapped
def enumerate_modules():
for name, m in module.iteritems():
print name
def dependencies(name):
for dep in module[name].dependencies:
print dep
def enumerate_outputs(*names):
if names:
modules = [module[name] for name in names]
modules = module.itervalues()
for m in modules:
for output in m.outputs:
print output
def build_module(name):
minifier = Uglify()
if __name__ == "__main__":
Jump to Line
Something went wrong with that request. Please try again.