OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
3 # for details. All rights reserved. Use of this source code is governed by a | |
4 # BSD-style license that can be found in the LICENSE file. | |
5 | |
6 # | |
7 | |
8 """Rewrites HTML files, converting Dart script sections into JavaScript. | |
9 | |
10 Process HTML files, and internally changes script sections that use Dart code | |
11 into JavaScript sections. It also can optimize the HTML to inline code. | |
12 """ | |
13 | |
14 from HTMLParser import HTMLParser | |
15 import os.path | |
16 from os.path import abspath, basename, dirname, exists, isabs, join | |
17 import base64, re, optparse, os, shutil, subprocess, sys, tempfile, codecs | |
18 import urllib2 | |
19 | |
20 CLIENT_PATH = dirname(dirname(abspath(__file__))) | |
21 DART_PATH = dirname(CLIENT_PATH) | |
22 TOOLS_PATH = join(DART_PATH, 'tools') | |
23 | |
24 sys.path.append(TOOLS_PATH) | |
25 import utils | |
26 | |
27 DART_MIME_TYPE = "application/dart" | |
28 LIBRARY_PATTERN = "^#library\(.*\);" | |
29 IMPORT_SOURCE_MATCHER = re.compile( | |
30 r"^ *(#import|#source)(\(['\"])([^'\"]*)(.*\);)", re.MULTILINE) | |
31 DOM_IMPORT_MATCHER = re.compile( | |
32 r"^#import\(['\"]dart\:dom_deprecated['\"].*\);", re.MULTILINE) | |
33 HTML_IMPORT_MATCHER = re.compile( | |
34 r"^#import\(['\"]dart\:html['\"].*\);", re.MULTILINE) | |
35 | |
36 FROG_NOT_FOUND_ERROR = ( | |
37 """Couldn't find compiler: please run the following commands: | |
38 $ cd %s | |
39 $ ./tools/build.py -m release""") | |
40 | |
41 ENTRY_POINT = """ | |
42 #library('entry'); | |
43 #import('%s', prefix: 'original'); | |
44 main() => original.main(); | |
45 """ | |
46 | |
47 CSS_TEMPLATE = '<style type="text/css">%s</style>' | |
48 CHROMIUM_SCRIPT_TEMPLATE = '<script type="application/javascript">%s</script>' | |
49 | |
50 DARTIUM_TO_JS_SCRIPT = """ | |
51 <script type="text/javascript"> | |
52 (function() { | |
53 // Let the user know that Dart is required. | |
54 if (!window.navigator.webkitStartDart) { | |
55 if (confirm( | |
56 "You are trying to run Dart code on a browser " + | |
57 "that doesn't support Dart. Do you want to redirect to " + | |
58 "a version compiled to JavaScript instead?")) { | |
59 var addr = window.location; | |
60 window.location = addr.toString().replace('-dart.html', '-js.html'); | |
61 } | |
62 } else { | |
63 window.navigator.webkitStartDart(); | |
64 } | |
65 })(); | |
66 </script> | |
67 """ | |
68 | |
69 def adjustImports(contents): | |
70 def repl(matchobj): | |
71 path = matchobj.group(3) | |
72 if not path.startswith('dart:'): | |
73 path = abspath(path) | |
74 return (matchobj.group(1) + matchobj.group(2) + path + matchobj.group(4)) | |
75 return IMPORT_SOURCE_MATCHER.sub(repl, contents) | |
76 | |
77 class DartCompiler(object): | |
78 """ Common code for compiling Dart script tags in an HTML file. """ | |
79 | |
80 def __init__(self, verbose=False, | |
81 extra_flags=""): | |
82 self.verbose = verbose | |
83 self.extra_flags = extra_flags | |
84 | |
85 def compileCode(self, src=None, body=None): | |
86 """ Compile the given source code. | |
87 | |
88 Either the script tag has a src attribute or a non-empty body (one of the | |
89 arguments will be none, the other is not). | |
90 | |
91 Args: | |
92 src: a string pointing to a Dart script file. | |
93 body: a string containing Dart code. | |
94 """ | |
95 | |
96 outdir = tempfile.mkdtemp() | |
97 indir = None | |
98 useDartHtml = False | |
99 if src is not None: | |
100 if body is not None and body.strip() != '': | |
101 raise ConverterException( | |
102 "The script body should be empty if src is specified") | |
103 elif src.endswith('.dart'): | |
104 indir = tempfile.mkdtemp() | |
105 inputfile = abspath(src) | |
106 with open(inputfile, 'r') as f: | |
107 contents = f.read(); | |
108 | |
109 if HTML_IMPORT_MATCHER.search(contents): | |
110 useDartHtml = True | |
111 | |
112 # We will import the source file to emulate in JS that code is run after | |
113 # DOMContentLoaded. We need a #library to ensure #import won't fail: | |
114 if not re.search(LIBRARY_PATTERN, contents, re.MULTILINE): | |
115 inputfile = join(indir, 'code.dart') | |
116 with open(inputfile, 'w') as f: | |
117 f.write("#library('code');") | |
118 f.write(adjustImports(contents)) | |
119 | |
120 else: | |
121 raise ConverterException("invalid file type:" + src) | |
122 else: | |
123 if body is None or body.strip() == '': | |
124 # nothing to do | |
125 print 'Warning: empty script tag with no src attribute' | |
126 return '' | |
127 | |
128 indir = tempfile.mkdtemp() | |
129 # eliminate leading spaces in front of directives | |
130 body = adjustImports(body) | |
131 | |
132 if HTML_IMPORT_MATCHER.search(body): | |
133 useDartHtml = True | |
134 | |
135 inputfile = join(indir, 'code.dart') | |
136 with open(inputfile, 'w') as f: | |
137 f.write("#library('inlinedcode');\n") | |
138 f.write(body) | |
139 | |
140 wrappedfile = join(indir, 'entry.dart') | |
141 with open(wrappedfile, 'w') as f: | |
142 f.write(ENTRY_POINT % inputfile) | |
143 | |
144 status, out, err = execute(self.compileCommand(wrappedfile, outdir), | |
145 self.verbose) | |
146 if status: | |
147 raise ConverterException('compilation errors') | |
148 | |
149 # Inline the compiled code in the page | |
150 with open(self.outputFileName(wrappedfile, outdir), 'r') as f: | |
151 res = f.read() | |
152 | |
153 # Cleanup | |
154 if indir is not None: | |
155 shutil.rmtree(indir) | |
156 shutil.rmtree(outdir) | |
157 return CHROMIUM_SCRIPT_TEMPLATE % res | |
158 | |
159 def compileCommand(self, inputfile, outdir): | |
160 binary = abspath(join(DART_PATH, | |
161 utils.GetBuildRoot(utils.GuessOS(), | |
162 'release', 'ia32'), | |
163 'dart-sdk', 'bin', 'dart2js')) | |
164 if not exists(binary): | |
165 raise ConverterException(FROG_NOT_FOUND_ERROR % DART_PATH) | |
166 | |
167 cmd = [binary, | |
168 '--out=' + self.outputFileName(inputfile, outdir)] | |
169 if self.extra_flags != "": | |
170 cmd.append(self.extra_flags); | |
171 cmd.append(inputfile) | |
172 return cmd | |
173 | |
174 def outputFileName(self, inputfile, outdir): | |
175 return join(outdir, basename(inputfile) + '.js') | |
176 | |
177 def execute(cmd, verbose=False): | |
178 """Execute a command in a subprocess. """ | |
179 if verbose: print 'Executing: ' + ' '.join(cmd) | |
180 try: | |
181 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
182 output, err = pipe.communicate() | |
183 if pipe.returncode != 0: | |
184 print 'Execution failed: ' + output + '\n' + err | |
185 if verbose or pipe.returncode != 0: | |
186 print output | |
187 print err | |
188 return pipe.returncode, output, err | |
189 except Exception as e: | |
190 print 'Exception when executing: ' + ' '.join(cmd) | |
191 print e | |
192 return 1, None, None | |
193 | |
194 | |
195 def convertPath(project_path, prefix_path): | |
196 """ Convert a project path (whose root corresponds to the current working | |
197 directory) to a system path. | |
198 Args: | |
199 - project_path: path in the project context. | |
200 - prefix_path: prefix for relative paths. | |
201 """ | |
202 if isabs(project_path): | |
203 # TODO(sigmund): add a flag to pass in the root-level for absolute paths. | |
204 return project_path[1:] | |
205 elif not (project_path.startswith('http://') or | |
206 project_path.startswith('https://')): | |
207 return join(prefix_path, project_path) | |
208 else: | |
209 return project_path | |
210 | |
211 def encodeImage(rootDir, filename): | |
212 """ Returns a base64 url encoding for an image """ | |
213 filetype = filename[-3:] | |
214 if filetype == 'svg': filetype = 'svg+xml' | |
215 with open(join(rootDir, filename), 'r') as f: | |
216 return 'url(data:image/%s;charset=utf-8;base64,%s)' % ( | |
217 filetype, | |
218 base64.b64encode(f.read())) | |
219 | |
220 def processCss(filename): | |
221 """ Reads and converts a css file by replacing all image refernces into | |
222 base64 encoded images. | |
223 """ | |
224 css = open(filename, 'r').read() | |
225 cssDir = os.path.split(filename)[0] | |
226 def transformUrl(match): | |
227 imagefile = match.group(1) | |
228 # if the image is not local or can't be found, leave the url alone: | |
229 if (imagefile.startswith('http://') | |
230 or imagefile.startswith('https://') | |
231 or not exists(join(cssDir, imagefile))): | |
232 return match.group(0) | |
233 return encodeImage(cssDir, imagefile) | |
234 | |
235 pattern = 'url\((.*\.(svg|png|jpg|gif))\)' | |
236 return re.sub(pattern, transformUrl, css) | |
237 | |
238 class DartHTMLConverter(HTMLParser): | |
239 """ An HTML processor that inlines css and compiled dart code. | |
240 | |
241 Args: | |
242 - compiler: an implementation of DartAnyCompiler | |
243 - prefix_path: prefix for relative paths encountered in the HTML. | |
244 """ | |
245 def __init__(self, compiler, prefix_path): | |
246 HTMLParser.__init__(self) | |
247 self.in_dart_tag = False | |
248 self.output = [] | |
249 self.dart_inline_code = [] | |
250 self.contains_dart = False | |
251 self.compiler = compiler | |
252 self.prefix_path = prefix_path | |
253 | |
254 def inlineCss(self, attrDic): | |
255 path = convertPath(attrDic['href'], self.prefix_path) | |
256 self.output.append(CSS_TEMPLATE % processCss(path)) | |
257 | |
258 def compileScript(self, attrDic): | |
259 if 'src' in attrDic: | |
260 self.output.append(self.compiler.compileCode( | |
261 src=convertPath(attrDic.pop('src'), self.prefix_path), | |
262 body=None)) | |
263 else: | |
264 self.in_dart_tag = True | |
265 # no tag is generated until we parse the body of the tag | |
266 self.dart_inline_code = [] | |
267 return True | |
268 | |
269 def convertImage(self, attrDic): | |
270 pass | |
271 | |
272 def starttagHelper(self, tag, attrs, isEnd): | |
273 attrDic = dict(attrs) | |
274 | |
275 # collect all script files, and generate a single script before </body> | |
276 if (tag == 'script' and 'type' in attrDic | |
277 and (attrDic['type'] == DART_MIME_TYPE)): | |
278 if self.compileScript(attrDic): | |
279 return | |
280 | |
281 # convert css imports into inlined css | |
282 elif (tag == 'link' and | |
283 'rel' in attrDic and attrDic['rel'] == 'stylesheet' and | |
284 'type' in attrDic and attrDic['type'] == 'text/css' and | |
285 'href' in attrDic): | |
286 self.inlineCss(attrDic) | |
287 return | |
288 | |
289 elif tag == 'img' and 'src' in attrDic: | |
290 self.convertImage(attrDic) | |
291 | |
292 # emit everything else as in the input | |
293 self.output.append('<%s%s%s>' % ( | |
294 tag + (' ' if len(attrDic) else ''), | |
295 ' '.join(['%s="%s"' % (k, attrDic[k]) for k in attrDic]), | |
296 '/' if isEnd else '')) | |
297 | |
298 def handle_starttag(self, tag, attrs): | |
299 self.starttagHelper(tag, attrs, False) | |
300 | |
301 def handle_startendtag(self, tag, attrs): | |
302 self.starttagHelper(tag, attrs, True) | |
303 | |
304 def handle_data(self, data): | |
305 if self.in_dart_tag: | |
306 # collect the dart source code and compile it all at once when no more | |
307 # script tags can be included. Note: the code will anyways start on | |
308 # DOMContentLoaded, so moving the script is OK. | |
309 self.dart_inline_code.append(data) | |
310 else: | |
311 self.output.append(data), | |
312 | |
313 def handle_endtag(self, tag): | |
314 if tag == 'script' and self.in_dart_tag: | |
315 self.in_dart_tag = False | |
316 self.output.append(self.compiler.compileCode( | |
317 src=None, body='\n'.join(self.dart_inline_code))) | |
318 else: | |
319 self.output.append('</%s>' % tag) | |
320 | |
321 def handle_charref(self, ref): | |
322 self.output.append('&#%s;' % ref) | |
323 | |
324 def handle_entityref(self, name): | |
325 self.output.append('&%s;' % name) | |
326 | |
327 def handle_comment(self, data): | |
328 self.output.append('<!--%s-->' % data) | |
329 | |
330 def handle_decl(self, decl): | |
331 self.output.append('<!%s>' % decl) | |
332 | |
333 def unknown_decl(self, data): | |
334 self.output.append('<!%s>' % data) | |
335 | |
336 def handle_pi(self, data): | |
337 self.output.append('<?%s>' % data) | |
338 | |
339 def getResult(self): | |
340 return ''.join(self.output) | |
341 | |
342 | |
343 class DartToDartHTMLConverter(DartHTMLConverter): | |
344 def __init__(self, prefix_path, outdir, verbose): | |
345 # Note: can't use super calls because HTMLParser is not a subclass of object | |
346 DartHTMLConverter.__init__(self, None, prefix_path) | |
347 self.outdir = outdir | |
348 self.verbose = verbose | |
349 | |
350 def compileScript(self, attrDic): | |
351 self.contains_dart = True | |
352 if 'src' in attrDic: | |
353 status, out, err = execute([ | |
354 sys.executable, | |
355 join(DART_PATH, 'tools', 'copy_dart.py'), | |
356 self.outdir, | |
357 convertPath(attrDic['src'], self.prefix_path)], | |
358 self.verbose) | |
359 | |
360 if status: | |
361 raise ConverterException('exception calling copy_dart.py') | |
362 | |
363 # do not rewrite the script tag | |
364 return False | |
365 | |
366 def handle_endtag(self, tag): | |
367 if tag == 'body' and self.contains_dart: | |
368 self.output.append(DARTIUM_TO_JS_SCRIPT) | |
369 DartHTMLConverter.handle_endtag(self, tag) | |
370 | |
371 # A data URL for a blank 1x1 PNG. The PNG's data is from | |
372 # convert -size 1x1 +set date:create +set date:modify \ | |
373 # xc:'rgba(0,0,0,0)' 1x1.png | |
374 # base64.b64encode(open('1x1.png').read()) | |
375 # (The +set stuff is because just doing "-strip" apparently doesn't work; | |
376 # it leaves several info chunks resulting in a 224-byte PNG.) | |
377 BLANK_IMAGE_BASE64_URL = 'data:image/png;charset=utf-8;base64,%s' % ( | |
378 ('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABEAQAAADljNBBAAAAAmJLR0T//xSrMc0AAAAJc' | |
379 'EhZcwAAAEgAAABIAEbJaz4AAAAJdnBBZwAAAAEAAAABAMeVX+0AAAANSURBVAjXY2BgYG' | |
380 'AAAAAFAAFe8yo6AAAAAElFTkSuQmCC')) | |
381 | |
382 class OfflineHTMLConverter(DartHTMLConverter): | |
383 def __init__(self, prefix_path, outdir, verbose, inline_images): | |
384 # Note: can't use super calls because HTMLParser is not a subclass of object | |
385 DartHTMLConverter.__init__(self, None, prefix_path) | |
386 self.outdir = outdir | |
387 self.verbose = verbose | |
388 self.inline_images = inline_images # Inline as data://, vs. use local file. | |
389 | |
390 def compileScript(self, attrDic): | |
391 # do not rewrite the script tag | |
392 return False | |
393 | |
394 def downloadImageUrlToEncode(self, url): | |
395 """ Downloads an image and returns a base64 url encoding for it. | |
396 May throw if the download fails. | |
397 """ | |
398 # Don't try to re-encode an image that's already data://. | |
399 filetype = url[-3:] | |
400 if filetype == 'svg': filetype = 'svg+xml' | |
401 if self.verbose: | |
402 print 'Downloading ' + url | |
403 f = urllib2.urlopen(url) | |
404 | |
405 return 'data:image/%s;charset=utf-8;base64,%s' % ( | |
406 filetype, | |
407 base64.b64encode(f.read())) | |
408 | |
409 def downloadImageUrlToFile(self, url): | |
410 """Downloads an image and returns the filename. May throw if the | |
411 download fails. | |
412 """ | |
413 extension = os.path.splitext(url)[1] | |
414 # mkstemp() happens to work to create a non-temporary, so we use it. | |
415 filename = tempfile.mkstemp(extension, 'img_', self.prefix_path)[1] | |
416 if self.verbose: | |
417 print 'Downloading %s to %s' % (url, filename) | |
418 writeOut(urllib2.urlopen(url).read(), filename) | |
419 return os.path.join(self.prefix_path, os.path.basename(filename)) | |
420 | |
421 def downloadImage(self, url): | |
422 """Downloads an image either to file or to data://, and return the URL.""" | |
423 if url.startswith('data:image/'): | |
424 return url | |
425 try: | |
426 if self.inline_images: | |
427 return self.downloadImageUrlToEncode(url) | |
428 else: | |
429 return self.downloadImageUrlToFile(url) | |
430 except: | |
431 print '*** Image download failed: %s' % url | |
432 return BLANK_IMAGE_BASE64_URL | |
433 | |
434 def convertImage(self, attrDic): | |
435 attrDic['src'] = self.downloadImage(attrDic['src']) | |
436 | |
437 def safeMakeDirs(dirname): | |
438 """ Creates a directory and, if necessary its parent directories. | |
439 | |
440 This function will safely return if other concurrent jobs try to create the | |
441 same directory. | |
442 """ | |
443 if not exists(dirname): | |
444 try: | |
445 os.makedirs(dirname) | |
446 except Exception: | |
447 # this check allows invoking this script concurrently in many jobs | |
448 if not exists(dirname): | |
449 raise | |
450 | |
451 class ConverterException(Exception): | |
452 """ An exception encountered during the convertion process """ | |
453 pass | |
454 | |
455 def Flags(): | |
456 """ Constructs a parser for extracting flags from the command line. """ | |
457 result = optparse.OptionParser() | |
458 result.add_option("--verbose", | |
459 help="Print verbose output", | |
460 default=False, | |
461 action="store_true") | |
462 result.add_option("-o", "--out", | |
463 help="Output directory", | |
464 type="string", | |
465 default=None, | |
466 action="store") | |
467 result.add_option("-t", "--target", | |
468 help="The target html to generate", | |
469 metavar="[js,chromium,dartium]", | |
470 default='chromium') | |
471 result.add_option("--extra-flags", | |
472 help="Extra flags for dart2js", | |
473 type="string", | |
474 default="") | |
475 result.set_usage("htmlconverter.py input.html -o OUTDIR") | |
476 return result | |
477 | |
478 def writeOut(contents, filepath): | |
479 """ Writes contents to a file, ensuring that the output directory exists. """ | |
480 safeMakeDirs(dirname(filepath)) | |
481 with open(filepath, 'w') as f: | |
482 f.write(contents) | |
483 print "Generated output in: " + abspath(filepath) | |
484 | |
485 def convertForDartium(filename, outdirBase, outfile, verbose): | |
486 """ Converts a file for a dartium target. """ | |
487 with open(filename, 'r') as f: | |
488 contents = f.read() | |
489 prefix_path = dirname(filename) | |
490 | |
491 # outdirBase is the directory to place all subdirectories for other dart files | |
492 # and resources. | |
493 converter = DartToDartHTMLConverter(prefix_path, outdirBase, verbose) | |
494 converter.feed(contents) | |
495 converter.close() | |
496 writeOut(converter.getResult(), outfile) | |
497 | |
498 def convertForChromium( | |
499 filename, extra_flags, outfile, verbose): | |
500 """ Converts a file for a chromium target. """ | |
501 with open(filename, 'r') as f: | |
502 contents = f.read() | |
503 prefix_path = dirname(filename) | |
504 converter = DartHTMLConverter( | |
505 DartCompiler(verbose, extra_flags), prefix_path) | |
506 converter.feed(contents) | |
507 converter.close() | |
508 writeOut(converter.getResult(), outfile) | |
509 | |
510 def convertForOffline(filename, outfile, verbose, encode_images): | |
511 """ Converts a file for offline use. """ | |
512 with codecs.open(filename, 'r', 'utf-8') as f: | |
513 contents = f.read() | |
514 converter = OfflineHTMLConverter(dirname(filename), | |
515 dirname(outfile), | |
516 verbose, | |
517 encode_images) | |
518 converter.feed(contents) | |
519 converter.close() | |
520 | |
521 contents = converter.getResult() | |
522 safeMakeDirs(dirname(outfile)) | |
523 with codecs.open(outfile, 'w', 'utf-8') as f: | |
524 f.write(contents) | |
525 print "Generated output in: " + abspath(outfile) | |
526 | |
527 RED_COLOR = "\033[31m" | |
528 NO_COLOR = "\033[0m" | |
529 | |
530 def main(): | |
531 parser = Flags() | |
532 options, args = parser.parse_args() | |
533 if len(args) < 1 or not options.out or not options.target: | |
534 parser.print_help() | |
535 return 1 | |
536 | |
537 try: | |
538 filename = args[0] | |
539 extension = filename[filename.rfind('.'):] | |
540 if extension != '.html' and extension != '.htm': | |
541 print "Invalid input file extension: %s" % extension | |
542 return 1 | |
543 outfile = join(options.out, filename) | |
544 if 'chromium' in options.target or 'js' in options.target: | |
545 convertForChromium(filename, | |
546 options.extra_flags, | |
547 outfile.replace(extension, '-js' + extension), options.verbose) | |
548 if 'dartium' in options.target: | |
549 convertForDartium(filename, options.out, | |
550 outfile.replace(extension, '-dart' + extension), options.verbose) | |
551 except Exception as e: | |
552 print "%sERROR%s: %s" % (RED_COLOR, NO_COLOR, str(e)) | |
553 return 1 | |
554 return 0 | |
555 | |
556 if __name__ == '__main__': | |
557 sys.exit(main()) | |
OLD | NEW |