| // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:html/dom.dart'; |
| import 'package:html/parser.dart' show parseFragment; |
| import 'package:logging/logging.dart' show Logger; |
| import 'package:path/path.dart' as path; |
| |
| import '../compiler.dart' show AbstractCompiler; |
| import '../server/dependency_graph.dart'; |
| import '../utils.dart' show colorOf, resourceOutputPath; |
| |
| /// Emits an entry point HTML file corresponding to [inputFile] that can load |
| /// the code generated by the dev compiler. |
| /// |
| /// This internally transforms the given HTML [document]. When compiling to |
| /// JavaScript, we remove any Dart script tags, add new script tags to load our |
| /// runtime and the compiled code, and to execute the main method of the |
| /// application. When compiling to Dart, we ensure that the document contains a |
| /// single Dart script tag, but otherwise emit the original document |
| /// unmodified. |
| String generateEntryHtml(HtmlSourceNode root, AbstractCompiler compiler) { |
| var options = compiler.options; |
| var document = root.document.clone(true); |
| var scripts = document.querySelectorAll('script[type="application/dart"]'); |
| if (scripts.isEmpty) { |
| _log.warning('No <script type="application/dart"> found in ${root.uri}'); |
| // TODO(jacobr): we would rather return document.outerHtml to avoid future |
| // bugs that would only show up with html files include references to Dart |
| // scripts. Passing through the input content is needed to avoid breaking |
| // Angular ecause the spec-compliant HTML parser used modifies the HTML |
| // in a way that breaks Angular templates. An alternate fix would be to |
| // write an HTML emitter that preserves the structure of the input HTML as |
| // much as possible. |
| return root.contents; |
| } |
| scripts.skip(1).forEach((s) { |
| // TODO(sigmund): allow more than one Dart script tags? |
| _log.warning(s.sourceSpan.message( |
| 'unexpected script. Only one Dart script tag allowed ' |
| '(see https://github.com/dart-lang/dart-dev-compiler/issues/53).', |
| color: options.useColors ? colorOf('warning') : false)); |
| s.remove(); |
| }); |
| |
| var libraries = []; |
| var resources = new Set(); |
| visitInPostOrder(root, (n) { |
| if (n is DartSourceNode) libraries.add(n); |
| if (n is ResourceSourceNode) resources.add(n); |
| }, includeParts: false); |
| |
| root.htmlResourceNodes.forEach((element, resource) { |
| // Make sure we don't try and add this node again. |
| resources.remove(resource); |
| |
| var resourcePath = |
| resourceOutputPath(resource.uri, root.uri, options.runtimeDir); |
| if (resource.cachingHash != null) { |
| resourcePath = _addHash(resourcePath, resource.cachingHash); |
| } |
| var attrs = element.attributes; |
| if (attrs.containsKey('href')) { |
| attrs['href'] = resourcePath; |
| } else if (attrs.containsKey('src')) { |
| attrs['src'] = resourcePath; |
| } |
| }); |
| |
| var rootDir = path.dirname(root.uri.path); |
| String rootRelative(String fullPath) { |
| return path.relative(path.join(compiler.inputBaseDir, fullPath), |
| from: rootDir); |
| } |
| |
| var fragment = new DocumentFragment(); |
| for (var resource in resources) { |
| var resourcePath = rootRelative( |
| resourceOutputPath(resource.uri, root.uri, options.runtimeDir)); |
| var ext = path.extension(resourcePath); |
| if (resource.cachingHash != null) { |
| resourcePath = _addHash(resourcePath, resource.cachingHash); |
| } |
| if (ext == '.js') { |
| fragment.nodes.add(libraryInclude(resourcePath)); |
| } else if (ext == '.css') { |
| var stylesheetLink = '<link rel="stylesheet" href="$resourcePath">\n'; |
| fragment.nodes.add(parseFragment(stylesheetLink)); |
| } |
| } |
| |
| String mainLibraryName; |
| var src = scripts[0].attributes["src"]; |
| var scriptUri = root.source.resolveRelativeUri(Uri.parse(src)); |
| |
| for (var lib in libraries) { |
| var info = lib.info; |
| if (info == null) continue; |
| var uri = info.library.source.uri; |
| var jsPath = rootRelative(compiler.getModulePath(uri)); |
| if (uri == scriptUri) mainLibraryName = compiler.getModuleName(uri); |
| if (lib.cachingHash != null) { |
| jsPath = _addHash(jsPath, lib.cachingHash); |
| } |
| fragment.nodes.add(libraryInclude(jsPath)); |
| } |
| fragment.nodes.add(invokeMain(mainLibraryName)); |
| scripts[0].replaceWith(fragment); |
| return '${document.outerHtml}\n'; |
| } |
| |
| // TODO(jmesserly): the string interpolation in these could lead to injection |
| // bugs. Not really a security issue since input is trusted, but the resulting |
| // parse tree may not match expectations if interpolated strings contain quotes. |
| |
| /// A script tag that loads the .js code for a compiled library. |
| Node libraryInclude(String jsUrl) => |
| parseFragment('<script src="$jsUrl"></script>\n'); |
| |
| /// A script tag that invokes the main function on the entry point library. |
| Node invokeMain(String mainLibraryName) { |
| var code = mainLibraryName == null |
| ? 'console.error("dev_compiler error: main was not generated");' |
| // TODO(vsm): Can we simplify this? |
| // See: https://github.com/dart-lang/dev_compiler/issues/164 |
| : "dart_library.start('$mainLibraryName');"; |
| return parseFragment('<script>$code</script>\n'); |
| } |
| |
| /// Convert the outputPath to include the hash in it. This function is the |
| /// reverse of what the server does to determine whether a request needs to have |
| /// cache headers added to it. |
| _addHash(String outPath, String hash) { |
| // (the ____ prefix makes it look better in the web inspector) |
| return '$outPath?____cached=$hash'; |
| } |
| |
| final _log = new Logger('dev_compiler.src.codegen.html_codegen'); |