PageRenderTime 26ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/r2/r2/lib/js.py

https://github.com/stevewilber/reddit
Python | 384 lines | 328 code | 23 blank | 33 comment | 18 complexity | 933eeba484062786fcc5d918d2976ded MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, Apache-2.0
  1. #!/usr/bin/env python
  2. # The contents of this file are subject to the Common Public Attribution
  3. # License Version 1.0. (the "License"); you may not use this file except in
  4. # compliance with the License. You may obtain a copy of the License at
  5. # http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
  6. # License Version 1.1, but Sections 14 and 15 have been added to cover use of
  7. # software over a computer network and provide for limited attribution for the
  8. # Original Developer. In addition, Exhibit A has been modified to be consistent
  9. # with Exhibit B.
  10. #
  11. # Software distributed under the License is distributed on an "AS IS" basis,
  12. # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
  13. # the specific language governing rights and limitations under the License.
  14. #
  15. # The Original Code is reddit.
  16. #
  17. # The Original Developer is the Initial Developer. The Initial Developer of
  18. # the Original Code is reddit Inc.
  19. #
  20. # All portions of the code written by reddit are Copyright (c) 2006-2012 reddit
  21. # Inc. All Rights Reserved.
  22. ###############################################################################
  23. import sys
  24. import os.path
  25. from subprocess import Popen, PIPE
  26. import re
  27. import json
  28. from r2.lib.translation import iter_langs
  29. from r2.lib.plugin import PluginLoader
  30. try:
  31. from pylons import g, c, config
  32. except ImportError:
  33. STATIC_ROOT = None
  34. else:
  35. STATIC_ROOT = config["pylons.paths"]["static_files"]
  36. # STATIC_ROOT will be None if pylons is uninitialized
  37. if not STATIC_ROOT:
  38. REDDIT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  39. STATIC_ROOT = os.path.join(os.path.dirname(REDDIT_ROOT), "build/public")
  40. script_tag = '<script type="text/javascript" src="{src}"></script>\n'
  41. inline_script_tag = '<script type="text/javascript">{content}</script>\n'
  42. class ClosureError(Exception): pass
  43. class ClosureCompiler(object):
  44. def __init__(self, jarpath, args=None):
  45. self.jarpath = jarpath
  46. self.args = args or []
  47. def _run(self, data, out=PIPE, args=None, expected_code=0):
  48. args = args or []
  49. p = Popen(["java", "-jar", self.jarpath] + self.args + args,
  50. stdin=PIPE, stdout=out, stderr=PIPE)
  51. out, msg = p.communicate(data)
  52. if p.returncode != expected_code:
  53. raise ClosureError(msg)
  54. else:
  55. return out, msg
  56. def compile(self, data, dest, args=None):
  57. """Run closure compiler on a string of source code `data`, writing the
  58. result to output file `dest`. A ClosureError exception will be raised if
  59. the operation is unsuccessful."""
  60. return self._run(data, dest, args)[0]
  61. class Source(object):
  62. """An abstract collection of JavaScript code."""
  63. def get_source(self):
  64. """Return the full JavaScript source code."""
  65. raise NotImplementedError
  66. def use(self):
  67. """Return HTML to insert the JavaScript source inside a template."""
  68. raise NotImplementedError
  69. @property
  70. def dependencies(self):
  71. raise NotImplementedError
  72. @property
  73. def outputs(self):
  74. raise NotImplementedError
  75. class FileSource(Source):
  76. """A JavaScript source file on disk."""
  77. def __init__(self, name):
  78. self.name = name
  79. def get_source(self):
  80. return open(self.path).read()
  81. @property
  82. def path(self):
  83. """The path to the source file on the filesystem."""
  84. return os.path.join(STATIC_ROOT, "static", "js", self.name)
  85. def use(self):
  86. from r2.lib.template_helpers import static
  87. path = [g.static_path, self.name]
  88. if g.uncompressedJS:
  89. path.insert(1, "js")
  90. return script_tag.format(src=static(os.path.join(*path)))
  91. @property
  92. def dependencies(self):
  93. return [self.path]
  94. class Module(Source):
  95. """A module of JS code consisting of a collection of sources."""
  96. def __init__(self, name, *sources, **kwargs):
  97. self.name = name
  98. self.should_compile = kwargs.get('should_compile', True)
  99. self.sources = []
  100. sources = sources or (name,)
  101. for source in sources:
  102. if not isinstance(source, Source):
  103. if 'prefix' in kwargs:
  104. source = os.path.join(kwargs['prefix'], source)
  105. source = FileSource(source)
  106. self.sources.append(source)
  107. def get_source(self):
  108. return ";".join(s.get_source() for s in self.sources)
  109. def extend(self, module):
  110. self.sources.extend(module.sources)
  111. @property
  112. def path(self):
  113. """The destination path of the module file on the filesystem."""
  114. return os.path.join(STATIC_ROOT, "static", self.name)
  115. def build(self, closure):
  116. with open(self.path, "w") as out:
  117. if self.should_compile:
  118. print >> sys.stderr, "Compiling {0}...".format(self.name),
  119. closure.compile(self.get_source(), out)
  120. else:
  121. print >> sys.stderr, "Concatenating {0}...".format(self.name),
  122. out.write(self.get_source())
  123. print >> sys.stderr, " done."
  124. def use(self):
  125. from r2.lib.template_helpers import static
  126. if g.uncompressedJS:
  127. return "".join(source.use() for source in self.sources)
  128. else:
  129. return script_tag.format(src=static(self.name))
  130. @property
  131. def dependencies(self):
  132. deps = []
  133. for source in self.sources:
  134. deps.extend(source.dependencies)
  135. return deps
  136. @property
  137. def outputs(self):
  138. return [self.path]
  139. class StringsSource(Source):
  140. """A virtual source consisting of localized strings from r2.lib.strings."""
  141. def __init__(self, lang=None, keys=None, prepend="r.strings = "):
  142. self.lang = lang
  143. self.keys = keys
  144. self.prepend = prepend
  145. def get_source(self):
  146. from pylons.i18n import get_lang
  147. from r2.lib import strings, translation
  148. if self.lang:
  149. old_lang = get_lang()
  150. translation.set_lang(self.lang)
  151. data = {}
  152. if self.keys is not None:
  153. for key in self.keys:
  154. data[key] = strings.strings[key]
  155. else:
  156. data = dict(strings.strings)
  157. output = self.prepend + json.dumps(data) + "\n"
  158. if self.lang:
  159. translation.set_lang(old_lang)
  160. return output
  161. def use(self):
  162. return inline_script_tag.format(content=self.get_source())
  163. class LocalizedModule(Module):
  164. """A module that is localized with r2.lib.strings.
  165. References to `r.strings.<string>` are parsed out of the module source.
  166. A StringsSource is created and included which contains localized versions
  167. of the strings referenced in the module.
  168. """
  169. @staticmethod
  170. def languagize_path(path, lang):
  171. path_name, path_ext = os.path.splitext(path)
  172. return path_name + "." + lang + path_ext
  173. def build(self, closure):
  174. Module.build(self, closure)
  175. reddit_source = open(self.path).read()
  176. string_keys = re.findall("r\.strings\.([\w$_]+)", reddit_source)
  177. print >> sys.stderr, "Creating language-specific files:"
  178. for lang, unused in iter_langs():
  179. strings = StringsSource(lang, string_keys)
  180. source = strings.get_source()
  181. lang_path = LocalizedModule.languagize_path(self.path, lang)
  182. # make sure we're not rewriting a different mangled file
  183. # via symlink
  184. if os.path.islink(lang_path):
  185. os.unlink(lang_path)
  186. with open(lang_path, "w") as out:
  187. print >> sys.stderr, " " + lang_path
  188. out.write(reddit_source+source)
  189. def use(self):
  190. from pylons.i18n import get_lang
  191. from r2.lib.template_helpers import static
  192. embed = Module.use(self)
  193. if g.uncompressedJS:
  194. return embed + StringsSource().use()
  195. else:
  196. langs = get_lang() or [g.lang]
  197. url = LocalizedModule.languagize_path(self.name, langs[0])
  198. return script_tag.format(src=static(url))
  199. @property
  200. def outputs(self):
  201. for lang, unused in iter_langs():
  202. yield LocalizedModule.languagize_path(self.path, lang)
  203. class JQuery(Module):
  204. version = "1.7.2"
  205. def __init__(self, cdn_url="http://ajax.googleapis.com/ajax/libs/jquery/{version}/jquery"):
  206. self.jquery_src = FileSource("lib/jquery-{0}.min.js".format(self.version))
  207. Module.__init__(self, "jquery.js", self.jquery_src, should_compile=False)
  208. self.cdn_src = cdn_url.format(version=self.version)
  209. def use(self):
  210. from r2.lib.template_helpers import static
  211. if c.secure or (c.user and c.user.pref_local_js):
  212. return Module.use(self)
  213. else:
  214. ext = ".js" if g.uncompressedJS else ".min.js"
  215. return script_tag.format(src=self.cdn_src+ext)
  216. module = {}
  217. module["jquery"] = JQuery()
  218. module["html5shiv"] = Module("html5shiv.js",
  219. "lib/html5shiv.js",
  220. should_compile=False
  221. )
  222. module["reddit"] = LocalizedModule("reddit.js",
  223. "lib/json2.js",
  224. "lib/underscore-1.3.3.js",
  225. "lib/store.js",
  226. "lib/jquery.cookie.js",
  227. "lib/jquery.url.js",
  228. "jquery.reddit.js",
  229. "base.js",
  230. "utils.js",
  231. "ui.js",
  232. "login.js",
  233. "analytics.js",
  234. "flair.js",
  235. "interestbar.js",
  236. "wiki.js",
  237. "reddit.js",
  238. "apps.js",
  239. "gold.js",
  240. )
  241. module["mobile"] = LocalizedModule("mobile.js",
  242. module["reddit"],
  243. "lib/jquery.lazyload.js",
  244. "compact.js"
  245. )
  246. module["button"] = Module("button.js",
  247. "lib/jquery.cookie.js",
  248. "jquery.reddit.js",
  249. "blogbutton.js"
  250. )
  251. module["sponsored"] = Module("sponsored.js",
  252. "lib/ui.core.js",
  253. "lib/ui.datepicker.js",
  254. "sponsored.js"
  255. )
  256. module["timeseries"] = Module("timeseries.js",
  257. "lib/jquery.flot.js",
  258. "lib/jquery.flot.time.js",
  259. "timeseries.js",
  260. )
  261. module["timeseries-ie"] = Module("timeseries-ie.js",
  262. "lib/excanvas.min.js",
  263. module["timeseries"],
  264. )
  265. module["traffic"] = LocalizedModule("traffic.js",
  266. "traffic.js",
  267. )
  268. module["qrcode"] = Module("qrcode.js",
  269. "lib/jquery.qrcode.min.js",
  270. "qrcode.js",
  271. )
  272. module["highlight"] = Module("highlight.js",
  273. "lib/highlight.pack.js",
  274. "highlight.js",
  275. )
  276. module["less"] = Module('less.js',
  277. 'lib/less-1.3.0.min.js',
  278. should_compile=False,
  279. )
  280. def use(*names):
  281. return "\n".join(module[name].use() for name in names)
  282. def load_plugin_modules(plugins=None):
  283. if not plugins:
  284. plugins = PluginLoader()
  285. for plugin in plugins:
  286. plugin.add_js(module)
  287. commands = {}
  288. def build_command(fn):
  289. def wrapped(*args):
  290. load_plugin_modules()
  291. fn(*args)
  292. commands[fn.__name__] = wrapped
  293. return wrapped
  294. @build_command
  295. def enumerate_modules():
  296. for name, m in module.iteritems():
  297. print name
  298. @build_command
  299. def dependencies(name):
  300. for dep in module[name].dependencies:
  301. print dep
  302. @build_command
  303. def enumerate_outputs(*names):
  304. if names:
  305. modules = [module[name] for name in names]
  306. else:
  307. modules = module.itervalues()
  308. for m in modules:
  309. for output in m.outputs:
  310. print output
  311. @build_command
  312. def build_module(name):
  313. closure = ClosureCompiler("r2/lib/contrib/closure_compiler/compiler.jar")
  314. module[name].build(closure)
  315. if __name__ == "__main__":
  316. commands[sys.argv[1]](*sys.argv[2:])