PageRenderTime 59ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/compilers/pyjsiocompile/pyjsiocompile/compile.py

https://github.com/skysbird/js.io
Python | 790 lines | 723 code | 33 blank | 34 comment | 67 complexity | 10b21f051166a54b8b18f84581ddb781 MD5 | raw file
  1. import logging
  2. import os
  3. import sys
  4. import re
  5. from urllib2 import urlopen
  6. import warnings
  7. import pprint
  8. fileopen = open
  9. # we need os.path.relpath, which isn't in python<2.6
  10. try:
  11. os.path.relpath
  12. except:
  13. # this function is taken directly from posixpath.py in 2.6
  14. # os.path added to abspath, commonprefix, curdir, pardir, sep, join
  15. def _relpath(path, start=os.path.curdir):
  16. """Return a relative version of a path"""
  17. if not path:
  18. raise ValueError("no path specified")
  19. start_list = os.path.abspath(start).split(os.path.sep)
  20. path_list = os.path.abspath(path).split(os.path.sep)
  21. # Work out how much of the filepath is shared by start and path.
  22. i = len(os.path.commonprefix([start_list, path_list]))
  23. rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:]
  24. if not rel_list:
  25. return os.path.curdir
  26. return os.path.join(*rel_list)
  27. os.path.relpath = _relpath
  28. from BeautifulSoup import BeautifulSoup as Soup
  29. try:
  30. import json
  31. except:
  32. import simplejson as json
  33. log = logging.getLogger(__name__)
  34. log.setLevel(logging.WARN)
  35. log.addHandler(logging.StreamHandler())
  36. class NoValue(object):
  37. pass
  38. def make_option_parser():
  39. from optparse import OptionParser
  40. parser = OptionParser("usage: %prog [options] inputfile")
  41. parser.add_option("-j", "--jsio-path",
  42. dest="jsio", type="string",
  43. default="http://js.io/svn/js.io/trunk/jsio",
  44. help="jsio source path")
  45. parser.add_option("-o", "--output",
  46. dest="output", type="string",
  47. default="output.js",
  48. help="output FILENAME", metavar="FILENAME")
  49. parser.add_option("-e", "--environments",
  50. dest="environments", type="string",
  51. action='append',
  52. default=[],
  53. help="target environments (e.g. browser or node)")
  54. parser.add_option("-t", "--transports",
  55. dest="transports", type="string",
  56. action="append",
  57. default=[],
  58. help="target transport (e.g. csp or tcp)")
  59. parser.add_option("--v",
  60. action="store_const", const=logging.INFO, dest="verbose")
  61. parser.add_option("--vv",
  62. action="store_const", const=logging.DEBUG, dest="verbose")
  63. parser.add_option("-d", "--dont-compress",
  64. action="store_false", dest="minify", default=True,
  65. help="Don't minify the output")
  66. parser.add_option("-G", "--google-rest-minify",
  67. action="store_true", dest="google_rest", default=False,
  68. help="Use Google Closure REST api to minify")
  69. parser.add_option("-g", "--google-minify",
  70. action="store_true", dest="google", default=False,
  71. help="Use Google compiler.jar to minify (must have java installed)")
  72. return parser
  73. def get_script_src_assignment(script_name):
  74. SCRIPT_NAME_ASSIGNMENT = u"jsio.script_src = '%s'"
  75. return SCRIPT_NAME_ASSIGNMENT % script_name
  76. def shellcmd(argv=None):
  77. main(argv)
  78. def main(argv=None):
  79. if argv == None:
  80. argv = sys.argv[1:]
  81. parser = make_option_parser()
  82. (options, args) = parser.parse_args(argv)
  83. log.debug(options)
  84. log.setLevel(options.verbose or logging.WARN)
  85. global minify
  86. if not options.minify:
  87. minify = lambda x, path=None: x
  88. elif options.google:
  89. minify = google_jar_minify
  90. elif options.google_rest:
  91. minify = google_minify
  92. if len(args) != 1:
  93. print "Invalid position arguments"
  94. parser.print_help()
  95. sys.exit(1)
  96. INPUT = args[0]
  97. OUTPUT = options.output
  98. options.initialImport = ""
  99. if INPUT.split('.')[-1] not in ('html', 'js', 'pkg'):
  100. print "Invalid input file; jsio_compile only operats on .js and .html files"
  101. sys.exit(1)
  102. compile_kwargs = {}
  103. if INPUT.endswith('pkg'):
  104. INPUT, options, compile_kwargs = \
  105. load_package_configuration(INPUT, options)
  106. output = \
  107. compile_source(INPUT, options, **compile_kwargs)
  108. # the root script needs to be able to recognize itself so that it can
  109. # figure out where it is. we modify the generated script to store the
  110. # expected script name. later on, we can compare that against script
  111. # tag src's.
  112. # output = \
  113. # output.replace(get_script_src_assignment('jsio.js'),
  114. # get_script_src_assignment(os.path.basename(OUTPUT)))
  115. # expose = re.compile('window\.jsio\s=\sjsio;');
  116. # expose = re.compile('this\.global\.jsio\s=\sjsio');
  117. # output = expose.sub(options.initialImport + (options.exposeJsio and ';this.global.jsio=jsio;' or ''), output, 1);
  118. output += options.initialImport;
  119. if options.minify:
  120. log.info("Minifying")
  121. output = minify(output, path='output')
  122. else:
  123. log.info("Skipping minify")
  124. print "Writing output %s" % OUTPUT
  125. # TODO: clean up this hack to write .html files back out
  126. if INPUT.endswith('.html'):
  127. orig_source = get_source(INPUT)
  128. soup = Soup(orig_source)
  129. orig_source = ""
  130. from BeautifulSoup import Tag
  131. tag1 = Tag(soup, "script")
  132. tag1.insert(0, output)
  133. soup.head.insert(0, tag1)
  134. output = soup.prettify()
  135. if OUTPUT:
  136. f = fileopen(OUTPUT, 'w')
  137. f.write(output)
  138. f.close()
  139. print "RETURNING", len(output)
  140. return output
  141. def load_package_configuration(INPUT, options):
  142. """ load the configuration options in the specified pkg
  143. the pkg options should take precedence over the command-line
  144. options
  145. """
  146. # QUESTION: is this true? are we really going to ignore command-line
  147. # options? let's at least print a message for now.
  148. pkg_data = json.loads(get_source(INPUT))
  149. pkg_data['root'] = str(pkg_data['root'])
  150. if 'environmenta' in pkg_data:
  151. print "using the 'environment' value from package %s" % INPUT
  152. options.environment = [str(env) for env in pkg_data['environments']]
  153. if 'transports' in pkg_data:
  154. print "using the 'transports' value from package %s" % INPUT
  155. options.transports = [str(xprt) for xprt in pkg_data['transports']]
  156. if 'environments' in pkg_data:
  157. print "using the 'environments' value from package %s" % INPUT
  158. options.environments = \
  159. [str(env) for env in pkg_data['environments']]
  160. options.initialImport = \
  161. '\njsio("import %s as %s");\n' % (pkg_data['root'], pkg_data.get('externalName', pkg_data['root']))
  162. options.exposeJsio = 'exposeJsio' in pkg_data and pkg_data['exposeJsio']
  163. BASEDIR = os.path.dirname(INPUT)
  164. new_input = join_paths(BASEDIR, pkg_data['root'] + '.js')
  165. return (new_input, options, dict(extras=[pkg_data['root']]))
  166. def join_paths(*paths):
  167. if '://' in paths[0]:
  168. return '/'.join(paths)
  169. else:
  170. return os.path.join(*paths)
  171. def minify(src, path=None):
  172. import StringIO
  173. jsm = JavascriptMinify()
  174. o = StringIO.StringIO()
  175. jsm.minify(StringIO.StringIO(src), o)
  176. return o.getvalue()
  177. def get_source(target):
  178. log.debug('fetching source from %s', target)
  179. if '://' in target:
  180. return urlopen(target).read()
  181. else:
  182. # print 'get_source', repr(target)
  183. return fileopen(target).read()
  184. def build_transport_paths(environments, transports):
  185. return ['net.env.%s.%s' % (environment, transport)
  186. for transport in transports
  187. for environment in environments]
  188. def get_transport_dependencies(jsio, env, xprts, path_template, extras=[]):
  189. dependencies = []
  190. for xprt in xprts:
  191. # print join_paths(jsio, 'net', 'env', env, xprt) + '.js'
  192. raw_source = \
  193. get_source(join_paths(jsio, 'net', 'env', env, xprt) + '.js')
  194. source = remove_comments(raw_source)
  195. dependencies.extend(map(lambda x: (x, path_template % env),
  196. (extract_dependencies(source))))
  197. return dependencies
  198. def get_dependencies(source, path='', extras=[]):
  199. # print '** get dependencies for', source
  200. return map(lambda x: (x, path),
  201. (extract_dependencies(source) + extras))
  202. def compile_source(target, options, extras=[]):
  203. log.info('compiling %s', target)
  204. orig_source = get_source(target)
  205. if target.endswith('.html'):
  206. soup = Soup(orig_source)
  207. orig_source = ""
  208. for script in select(soup, 'script'):
  209. if 'src' in dict(script.attrs):
  210. continue
  211. orig_source += script.contents[0] + "\n\n"
  212. target_source = remove_comments(orig_source)
  213. target_module = os.path.relpath(target).split('/')[-1].split('.')[0]
  214. target_module_path = target_module + '.js'
  215. dependencies = get_dependencies(target_source, extras)
  216. dependencies.append(('jsio','jsio.jsio'))
  217. # print 'dependencies:', dependencies
  218. log.info('dependencies:')
  219. log.info(pprint.pformat(dependencies))
  220. checked = [target_module, 'net.env', 'log', 'Class', 'bind']
  221. transport_paths = build_transport_paths(options.environments,
  222. options.transports)
  223. checked.extend(transport_paths)
  224. for environment in options.environments:
  225. dependencies.extend(\
  226. get_transport_dependencies(options.jsio,
  227. environment,
  228. options.transports,
  229. 'net.env.%s.'))
  230. log.debug('checked is %s', checked)
  231. while dependencies:
  232. pkg, path = dependencies.pop(0)
  233. # if isinstance(pkg, list):
  234. # # This just means that we are analyzing a import path that we
  235. # # extracted from the target/root file
  236. # if path[0] == '.':
  237. # full_path =
  238. # print 'pkg, path', pkg, path
  239. full_path = joinModulePath(path, pkg)
  240. if full_path == 'jsio':
  241. full_path = 'jsio.jsio'
  242. log.debug('full_path: %s', full_path)
  243. if full_path in checked:
  244. continue
  245. log.debug('getting dependancies for %s', full_path)
  246. target = path_for_module(full_path, prefix=options.jsio)
  247. src = remove_comments(get_source(target))
  248. depends = map(lambda x: (x, full_path), extract_dependencies(src))
  249. dependencies.extend(depends)
  250. if not pkg == 'jsio':
  251. checked.append(full_path)
  252. sources = {}
  253. sources[target_module] = dict(src=minify(target_source,path=target_module_path),
  254. filePath=target_module_path)
  255. log.debug('checked is %s', checked)
  256. for full_path in checked:
  257. if full_path in (target_module, 'jsio', # 'jsio.env',
  258. 'log', 'Class', 'bind'):
  259. continue
  260. filename = path_for_module(full_path, prefix=options.jsio)
  261. log.info("Loading dependancy %s from %s", full_path, filename)
  262. src = get_source(filename)
  263. virtual_filename = path_for_module(full_path, prefix='jsio')
  264. log.debug(virtual_filename)
  265. sources[full_path] = {'src': minify(src,path=virtual_filename), 'filePath': virtual_filename, }
  266. out = ',\n'.join([ repr(str(key)) + ": " + json.dumps(val)
  267. for (key, val) in sources.items() ])
  268. jsio_src = get_source(join_paths(options.jsio, 'jsio.js'))
  269. final_output = \
  270. jsio_src.replace(" // Insert pre-loaded modules here...", out)
  271. return final_output
  272. def path_for_module(full_path, prefix):
  273. # print 'path_for_module', full_path
  274. is_relative = full_path[0] == '.'
  275. path_components = full_path.split('.')
  276. if full_path == 'jsio':
  277. path_components = [prefix, full_path]
  278. elif (path_components[0] == 'jsio'):
  279. path_components[0] = prefix
  280. elif not is_relative:
  281. path_components.insert(0, prefix)
  282. #log.info('TEST', path_components)
  283. path_components = [path_components[0]] + [ p or '..' for p in path_components[1:] ]
  284. return join_paths(*path_components) + '.js'
  285. def joinModulePath(a, b):
  286. if b[0] != '.':
  287. return b
  288. if isinstance(a, list):
  289. return b
  290. # print 'joinModule', a, b
  291. segments = a.split('.')
  292. while b[0] == '.':
  293. b = b[1:]
  294. segments.pop()
  295. output = '.'.join(segments) + '.' + b
  296. if output[0] == '.':
  297. output = output[1:]
  298. # print 'return', output
  299. return output
  300. def extract_dependencies(src):
  301. dependencies = []
  302. re1 = re.compile("jsio\(\s*['\"]\s*(from|external)\s+([\w.$]+)\s+import\s+(.*?)\s*['\"]\s*\)")
  303. for item in re1.finditer(src):
  304. dependencies.append(item.groups()[1])
  305. re2 = re.compile("jsio\(\s*['\"]\s*import\s+(.*?)\s*['\"]\s*\)")
  306. re3 = re.compile("\s*([\w.$]+)(?:\s+as\s+([\w.$]+))?,?")
  307. for item in re2.finditer(src):
  308. for listItem in re3.finditer(item.groups()[0]):
  309. dependencies.append(listItem.groups()[0])
  310. log.debug('deps: ' + ', '.join(dependencies))
  311. return dependencies
  312. def remove_comments(src):
  313. # new regular expression way here... -mario
  314. RE_COMMENT = re.compile(r'([^\\]/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/)|([^:\\]//.*)')
  315. comment = RE_COMMENT.search(src)
  316. while comment:
  317. src = src[:comment.start()] + src[comment.end():]
  318. comment = RE_COMMENT.search(src)
  319. # hack for now: RE_COMMENT only matches comments not found at the start of the file, so explicitly look for those here
  320. RE_COMMENT = re.compile(r'^/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/')
  321. comment = RE_COMMENT.search(src)
  322. if comment:
  323. src = src[:comment.start()] + src[comment.end():]
  324. return src
  325. # old way below here (brutally deletes http://'s)... -mario
  326. """
  327. output = ""
  328. i = 0
  329. while True:
  330. j = src.find('/*', i)
  331. if j == -1:
  332. output += src[i:]
  333. break
  334. output += src[i:j]
  335. k = src.find('*/', i)
  336. if k == -1:
  337. print 'unterminated comment detected'
  338. sys.exit(0)
  339. i = k+2
  340. output2 = ""
  341. for line in output.split('\n'):
  342. # XXX: Won't quite work with strings...
  343. line = line.split('//')[0]
  344. if line:
  345. output2 += line + '\n'
  346. return output2
  347. """
  348. """
  349. soupselect.py
  350. CSS selector support for BeautifulSoup.
  351. soup = BeautifulSoup('<html>...')
  352. select(soup, 'div')
  353. - returns a list of div elements
  354. select(soup, 'div#main ul a')
  355. - returns a list of links inside a ul inside div#main
  356. """
  357. import re
  358. tag_re = re.compile('^[a-z0-9]+$')
  359. attribselect_re = re.compile(
  360. r'^(?P<tag>\w+)?\[(?P<attribute>\w+)(?P<operator>[=~\|\^\$\*]?)' +
  361. r'=?"?(?P<value>[^\]"]*)"?\]$'
  362. )
  363. # /^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
  364. # \---/ \---/\-------------/ \-------/
  365. # | | | |
  366. # | | | The value
  367. # | | ~,|,^,$,* or =
  368. # | Attribute
  369. # Tag
  370. def attribute_checker(operator, attribute, value=''):
  371. """
  372. Takes an operator, attribute and optional value; returns a function that
  373. will return True for elements that match that combination.
  374. """
  375. return {
  376. '=': lambda el: el.get(attribute) == value,
  377. # attribute includes value as one of a set of space separated tokens
  378. '~': lambda el: value in el.get(attribute, '').split(),
  379. # attribute starts with value
  380. '^': lambda el: el.get(attribute, '').startswith(value),
  381. # attribute ends with value
  382. '$': lambda el: el.get(attribute, '').endswith(value),
  383. # attribute contains value
  384. '*': lambda el: value in el.get(attribute, ''),
  385. # attribute is either exactly value or starts with value-
  386. '|': lambda el: el.get(attribute, '') == value \
  387. or el.get(attribute, '').startswith('%s-' % value),
  388. }.get(operator, lambda el: el.has_key(attribute))
  389. def select(soup, selector):
  390. """
  391. soup should be a BeautifulSoup instance; selector is a CSS selector
  392. specifying the elements you want to retrieve.
  393. """
  394. tokens = selector.split()
  395. current_context = [soup]
  396. for token in tokens:
  397. m = attribselect_re.match(token)
  398. if m:
  399. # Attribute selector
  400. tag, attribute, operator, value = m.groups()
  401. if not tag:
  402. tag = True
  403. checker = attribute_checker(operator, attribute, value)
  404. found = []
  405. for context in current_context:
  406. found.extend([el for el in context.findAll(tag) if checker(el)])
  407. current_context = found
  408. continue
  409. if '#' in token:
  410. # ID selector
  411. tag, id = token.split('#', 1)
  412. if not tag:
  413. tag = True
  414. el = current_context[0].find(tag, {'id': id})
  415. if not el:
  416. return [] # No match
  417. current_context = [el]
  418. continue
  419. if '.' in token:
  420. # Class selector
  421. tag, klass = token.split('.', 1)
  422. if not tag:
  423. tag = True
  424. found = []
  425. for context in current_context:
  426. found.extend(
  427. context.findAll(tag,
  428. {'class': lambda attr: attr and klass in attr.split()}
  429. )
  430. )
  431. current_context = found
  432. continue
  433. if token == '*':
  434. # Star selector
  435. found = []
  436. for context in current_context:
  437. found.extend(context.findAll(True))
  438. current_context = found
  439. continue
  440. # Here we should just have a regular tag
  441. if not tag_re.match(token):
  442. return []
  443. found = []
  444. for context in current_context:
  445. found.extend(context.findAll(token))
  446. current_context = found
  447. return current_context
  448. def monkeypatch(BeautifulSoupClass=None):
  449. """
  450. If you don't explicitly state the class to patch, defaults to the most
  451. common import location for BeautifulSoup.
  452. """
  453. if not BeautifulSoupClass:
  454. from BeautifulSoup import BeautifulSoup as BeautifulSoupClass
  455. BeautifulSoupClass.findSelect = select
  456. def unmonkeypatch(BeautifulSoupClass=None):
  457. if not BeautifulSoupClass:
  458. from BeautifulSoup import BeautifulSoup as BeautifulSoupClass
  459. delattr(BeautifulSoupClass, 'findSelect')
  460. def google_jar_minify(src, path=None):
  461. import os, subprocess
  462. compiler = os.path.join(os.path.dirname(__file__), 'compiler.jar')
  463. if not os.path.exists(compiler):
  464. compiler = 'compiler.jar'
  465. if not os.path.exists(compiler):
  466. log.critical('ERROR: could not find google closure\'s compiler.jar for -g option')
  467. sys.exit(1)
  468. compilation_level = 'SIMPLE_OPTIMIZATIONS' # 'ADVANCED_OPTIMIZATIONS', 'WHITESPACE_ONLY'
  469. warning_level = 'DEFAULT' # 'QUIET', 'VERBOSE'
  470. log.info('closure compiler minifying %s', path)
  471. command = ['java', '-jar', compiler, '--compilation_level', compilation_level, '--warning_level', warning_level]
  472. handle = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  473. stdout, stderr = handle.communicate(src)
  474. if handle.returncode:
  475. log.error('Failed compiling %s', path)
  476. log.error(stderr)
  477. sys.exit(handle.returncode)
  478. elif stderr:
  479. log.warn('compiler warnings for %s', path)
  480. log.warn(stderr)
  481. return stdout
  482. def google_minify(src, path=None):
  483. print '*Google minify'
  484. # Code from http://code.google.com/closure/compiler/docs/api-tutorial1.html
  485. import httplib, urllib, sys
  486. # Define the parameters for the POST request and encode them in
  487. # a URL-safe format.
  488. params = urllib.urlencode([
  489. ('js_code', src),
  490. ('compilation_level', 'SIMPLE_OPTIMIZATIONS'),
  491. ('output_format', 'text'),
  492. ('output_info', 'compiled_code'),
  493. ])
  494. # Always use the following value for the Content-type header.
  495. headers = { "Content-type": "application/x-www-form-urlencoded" }
  496. conn = httplib.HTTPConnection('closure-compiler.appspot.com')
  497. conn.request('POST', '/compile', params, headers)
  498. response = conn.getresponse()
  499. data = response.read()
  500. if len(data) < 70:
  501. print 'failed'
  502. return data
  503. # conn.close()
  504. #!/usr/bin/python
  505. # This code is original from jsmin by Douglas Crockford, it was translated to
  506. # Python by Baruch Even. The original code had the following copyright and
  507. # license.
  508. #
  509. # /* jsmin.c
  510. # 2007-05-22
  511. #
  512. # Copyright (c) 2002 Douglas Crockford (www.crockford.com)
  513. #
  514. # Permission is hereby granted, free of charge, to any person obtaining a copy of
  515. # this software and associated documentation files (the "Software"), to deal in
  516. # the Software without restriction, including without limitation the rights to
  517. # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  518. # of the Software, and to permit persons to whom the Software is furnished to do
  519. # so, subject to the following conditions:
  520. #
  521. # The above copyright notice and this permission notice shall be included in all
  522. # copies or substantial portions of the Software.
  523. #
  524. # The Software shall be used for Good, not Evil.
  525. #
  526. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  527. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  528. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  529. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  530. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  531. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  532. # SOFTWARE.
  533. # */
  534. from StringIO import StringIO
  535. # def jsmin(js):
  536. # ins = StringIO(js)
  537. # outs = StringIO()
  538. # JavascriptMinify().minify(ins, outs)
  539. # str = outs.getvalue()
  540. # if len(str) > 0 and str[0] == '\n':
  541. # str = str[1:]
  542. # return str
  543. def isAlphanum(c):
  544. """return true if the character is a letter, digit, underscore,
  545. dollar sign, or non-ASCII character.
  546. """
  547. return ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or
  548. (c >= 'A' and c <= 'Z') or c == '_' or c == '$' or c == '\\' or (c is not None and ord(c) > 126));
  549. class UnterminatedComment(Exception):
  550. pass
  551. class UnterminatedStringLiteral(Exception):
  552. pass
  553. class UnterminatedRegularExpression(Exception):
  554. pass
  555. class JavascriptMinify(object):
  556. def _outA(self):
  557. self.outstream.write(self.theA)
  558. def _outB(self):
  559. self.outstream.write(self.theB)
  560. def _get(self):
  561. """return the next character from stdin. Watch out for lookahead. If
  562. the character is a control character, translate it to a space or
  563. linefeed.
  564. """
  565. c = self.theLookahead
  566. self.theLookahead = None
  567. if c == None:
  568. c = self.instream.read(1)
  569. if c >= ' ' or c == '\n':
  570. return c
  571. if c == '': # EOF
  572. return '\000'
  573. if c == '\r':
  574. return '\n'
  575. return ' '
  576. def _peek(self):
  577. self.theLookahead = self._get()
  578. return self.theLookahead
  579. def _next(self):
  580. """get the next character, excluding comments. peek() is used to see
  581. if an unescaped '/' is followed by a '/' or '*'.
  582. """
  583. c = self._get()
  584. if c == '/' and self.theA != '\\':
  585. p = self._peek()
  586. if p == '/':
  587. c = self._get()
  588. while c > '\n':
  589. c = self._get()
  590. return c
  591. if p == '*':
  592. c = self._get()
  593. while 1:
  594. c = self._get()
  595. if c == '*':
  596. if self._peek() == '/':
  597. self._get()
  598. return ' '
  599. if c == '\000':
  600. raise UnterminatedComment()
  601. return c
  602. def _action(self, action):
  603. """do something! What you do is determined by the argument:
  604. 1 Output A. Copy B to A. Get the next B.
  605. 2 Copy B to A. Get the next B. (Delete A).
  606. 3 Get the next B. (Delete B).
  607. action treats a string as a single character. Wow!
  608. action recognizes a regular expression if it is preceded by ( or , or =.
  609. """
  610. if action <= 1:
  611. self._outA()
  612. if action <= 2:
  613. self.theA = self.theB
  614. if self.theA == "'" or self.theA == '"':
  615. while 1:
  616. self._outA()
  617. self.theA = self._get()
  618. if self.theA == self.theB:
  619. break
  620. if self.theA <= '\n':
  621. raise UnterminatedStringLiteral()
  622. if self.theA == '\\':
  623. self._outA()
  624. self.theA = self._get()
  625. if action <= 3:
  626. self.theB = self._next()
  627. if self.theB == '/' and (self.theA == '(' or self.theA == ',' or
  628. self.theA == '=' or self.theA == ':' or
  629. self.theA == '[' or self.theA == '?' or
  630. self.theA == '!' or self.theA == '&' or
  631. self.theA == '|' or self.theA == ';' or
  632. self.theA == '{' or self.theA == '}' or
  633. self.theA == '\n'):
  634. self._outA()
  635. self._outB()
  636. while 1:
  637. self.theA = self._get()
  638. if self.theA == '/':
  639. break
  640. elif self.theA == '\\':
  641. self._outA()
  642. self.theA = self._get()
  643. elif self.theA <= '\n':
  644. raise UnterminatedRegularExpression()
  645. self._outA()
  646. self.theB = self._next()
  647. def _jsmin(self):
  648. """Copy the input to the output, deleting the characters which are
  649. insignificant to JavaScript. Comments will be removed. Tabs will be
  650. replaced with spaces. Carriage returns will be replaced with linefeeds.
  651. Most spaces and linefeeds will be removed.
  652. """
  653. self.theA = '\n'
  654. self._action(3)
  655. while self.theA != '\000':
  656. if self.theA == ' ':
  657. if isAlphanum(self.theB):
  658. self._action(1)
  659. else:
  660. self._action(2)
  661. elif self.theA == '\n':
  662. if self.theB in ['{', '[', '(', '+', '-']:
  663. self._action(1)
  664. elif self.theB == ' ':
  665. self._action(3)
  666. else:
  667. if isAlphanum(self.theB):
  668. self._action(1)
  669. else:
  670. self._action(2)
  671. else:
  672. if self.theB == ' ':
  673. if isAlphanum(self.theA):
  674. self._action(1)
  675. else:
  676. self._action(3)
  677. elif self.theB == '\n':
  678. if self.theA in ['}', ']', ')', '+', '-', '"', '\'']:
  679. self._action(1)
  680. else:
  681. if isAlphanum(self.theA):
  682. self._action(1)
  683. else:
  684. self._action(3)
  685. else:
  686. self._action(1)
  687. def minify(self, instream, outstream):
  688. self.instream = instream
  689. self.outstream = outstream
  690. self.theA = '\n'
  691. self.theB = None
  692. self.theLookahead = None
  693. self._jsmin()
  694. self.instream.close()
  695. if __name__ == "__main__":
  696. main()