blob: 9d5d10de5ed1168d0dd1eeec801ba2b26e4fcfde [file] [log] [blame]
// Copyright (c) 2012, 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.
library compiler;
import 'dart:async';
import 'dart:collection' show SplayTreeMap;
import 'package:csslib/visitor.dart' show StyleSheet, treeToDebugString;
import 'package:html5lib/dom.dart';
import 'package:html5lib/parser.dart';
import 'analyzer.dart';
import 'css_analyzer.dart' show analyzeCss, findUrlsImported,
findImportsInStyleSheet, parseCss;
import 'css_emitters.dart' show rewriteCssUris,
emitComponentStyleSheet, emitOriginalCss, emitStyleSheet;
import 'file_system.dart';
import 'files.dart';
import 'info.dart';
import 'messages.dart';
import 'compiler_options.dart';
import 'utils.dart';
/**
* Parses an HTML file [contents] and returns a DOM-like tree.
* Note that [contents] will be a [String] if coming from a browser-based
* [FileSystem], or it will be a [List<int>] if running on the command line.
*
* Adds emitted error/warning to [messages], if [messages] is supplied.
*/
Document parseHtml(contents, String sourcePath, Messages messages,
bool checkDocType) {
var parser = new HtmlParser(contents, generateSpans: true,
sourceUrl: sourcePath);
var document = parser.parse();
// Note: errors aren't fatal in HTML (unless strict mode is on).
// So just print them as warnings.
for (var e in parser.errors) {
if (checkDocType || e.errorCode != 'expected-doctype-but-got-start-tag') {
messages.warning(e.message, e.span);
}
}
return document;
}
/** Compiles an application written with Dart web components. */
class Compiler {
final FileSystem fileSystem;
final CompilerOptions options;
final List<SourceFile> files = <SourceFile>[];
String _mainPath;
String _packageRoot;
String _resetCssFile;
StyleSheet _cssResetStyleSheet;
Messages _messages;
FutureGroup _tasks;
Set _processed;
/** Information about source [files] given their href. */
final Map<String, FileInfo> info = new SplayTreeMap<String, FileInfo>();
final GlobalInfo global = new GlobalInfo();
/** Creates a compiler with [options] using [fileSystem]. */
Compiler(this.fileSystem, this.options, this._messages) {
_mainPath = options.inputFile;
var mainDir = path.dirname(_mainPath);
var baseDir = options.baseDir != null ? options.baseDir : mainDir;
_packageRoot = options.packageRoot != null ? options.packageRoot
: path.join(path.dirname(_mainPath), 'packages');
if (options.resetCssFile != null) {
_resetCssFile = options.resetCssFile;
if (path.isRelative(_resetCssFile)) {
// If CSS reset file path is relative from our current path.
_resetCssFile = path.resolve(_resetCssFile);
}
}
// Normalize paths - all should be relative or absolute paths.
if (path.isAbsolute(_mainPath) || path.isAbsolute(baseDir)
|| path.isAbsolute(_packageRoot)) {
if (path.isRelative(_mainPath)) _mainPath = path.resolve(_mainPath);
if (path.isRelative(baseDir)) baseDir = path.resolve(baseDir);
if (path.isRelative(_packageRoot)) {
_packageRoot = path.resolve(_packageRoot);
}
}
}
/** Compile the application starting from the given input file. */
Future run() {
if (path.basename(_mainPath).endsWith('.dart')) {
_messages.error("Please provide an HTML file as your entry point.",
null);
return new Future.value(null);
}
return _parseAndDiscover(_mainPath).then((_) {
_analyze();
// Analyze all CSS files.
_time('Analyzed Style Sheets', '', () =>
analyzeCss(_packageRoot, files, info,
global.pseudoElements, _messages,
warningsAsErrors: options.warningsAsErrors));
});
}
/**
* Asynchronously parse [inputFile] and transitively discover web components
* to load and parse. Returns a future that completes when all files are
* processed.
*/
Future _parseAndDiscover(String inputFile) {
_tasks = new FutureGroup();
_processed = new Set();
_processed.add(inputFile);
_tasks.add(_parseHtmlFile(new UrlInfo(inputFile, inputFile, null), true));
return _tasks.future;
}
void _processHtmlFile(UrlInfo inputUrl, SourceFile file) {
if (file == null) return;
bool isEntryPoint = _processed.length == 1;
files.add(file);
var fileInfo = _time('Analyzed definitions', inputUrl.url, () {
return analyzeDefinitions(global, inputUrl, file.document, _packageRoot,
_messages);
});
info[inputUrl.resolvedPath] = fileInfo;
if (isEntryPoint && _resetCssFile != null) {
_processed.add(_resetCssFile);
_tasks.add(_parseCssFile(new UrlInfo(_resetCssFile, _resetCssFile,
null)));
}
// Load component files referenced by [file].
for (var link in fileInfo.componentLinks) {
_loadFile(link, _parseHtmlFile);
}
// Load stylesheet files referenced by [file].
for (var link in fileInfo.styleSheetHrefs) {
_loadFile(link, _parseCssFile);
}
// Process any @imports inside of a <style> tag.
var urlInfos = findUrlsImported(fileInfo, fileInfo.inputUrl, _packageRoot,
file.document, _messages, options);
for (var urlInfo in urlInfos) {
_loadFile(urlInfo, _parseCssFile);
}
// Load .dart files being referenced in components.
for (var component in fileInfo.declaredComponents) {
// Process any @imports inside of the <style> tag in a component.
var urlInfos = findUrlsImported(component, fileInfo.inputUrl,
_packageRoot, component.element, _messages, options);
for (var urlInfo in urlInfos) {
_loadFile(urlInfo, _parseCssFile);
}
}
}
/**
* Helper function to load [urlInfo] and parse it using [loadAndParse] if it
* hasn't been loaded before.
*/
void _loadFile(UrlInfo urlInfo, Future loadAndParse(UrlInfo inputUrl)) {
if (urlInfo == null) return;
var resolvedPath = urlInfo.resolvedPath;
if (!_processed.contains(resolvedPath)) {
_processed.add(resolvedPath);
_tasks.add(loadAndParse(urlInfo));
}
}
/** Parse an HTML file. */
Future _parseHtmlFile(UrlInfo inputUrl, [bool checkDocType = false]) {
var filePath = inputUrl.resolvedPath;
return fileSystem.readTextOrBytes(filePath)
.catchError((e) => _readError(e, inputUrl))
.then((source) {
if (source == null) return;
var file = new SourceFile(filePath);
file.document = _time('Parsed', filePath,
() => parseHtml(source, filePath, _messages, checkDocType));
_processHtmlFile(inputUrl, file);
});
}
/** Parse a stylesheet file. */
Future _parseCssFile(UrlInfo inputUrl) {
if (!options.emulateScopedCss) {
return new Future<SourceFile>.value(null);
}
var filePath = inputUrl.resolvedPath;
return fileSystem.readText(filePath)
.catchError((e) => _readError(e, inputUrl, isWarning: true))
.then((code) {
if (code == null) return;
var file = new SourceFile(filePath, type: SourceFile.STYLESHEET);
file.code = code;
_processCssFile(inputUrl, file);
});
}
SourceFile _readError(error, UrlInfo inputUrl, {isWarning: false}) {
var message = 'unable to open file "${inputUrl.resolvedPath}"';
if (options.verbose) {
message = '$message. original message:\n $error';
}
if (isWarning) {
_messages.warning(message, inputUrl.sourceSpan);
} else {
_messages.error(message, inputUrl.sourceSpan);
}
return null;
}
void _processCssFile(UrlInfo inputUrl, SourceFile cssFile) {
if (cssFile == null) return;
files.add(cssFile);
var fileInfo = new FileInfo(inputUrl);
info[inputUrl.resolvedPath] = fileInfo;
var styleSheet = parseCss(cssFile.code, _messages, options);
if (inputUrl.url == _resetCssFile) {
_cssResetStyleSheet = styleSheet;
} else if (styleSheet != null) {
_resolveStyleSheetImports(inputUrl, cssFile.path, styleSheet);
fileInfo.styleSheets.add(styleSheet);
}
}
/** Load and parse all style sheets referenced with an @imports. */
void _resolveStyleSheetImports(UrlInfo inputUrl, String processingFile,
StyleSheet styleSheet) {
var urlInfos = _time('CSS imports', processingFile, () =>
findImportsInStyleSheet(styleSheet, _packageRoot, inputUrl, _messages));
for (var urlInfo in urlInfos) {
if (urlInfo == null) break;
// Load any @imported stylesheet files referenced in this style sheet.
_loadFile(urlInfo, _parseCssFile);
}
}
/** Run the analyzer on every input html file. */
void _analyze() {
var uniqueIds = new IntIterator();
for (var file in files) {
if (file.isHtml) {
_time('Analyzed contents', file.path, () =>
analyzeFile(file, info, uniqueIds, global, _messages,
options.emulateScopedCss));
}
}
}
// TODO(jmesserly): refactor this and other CSS related transforms out of
// Compiler.
/**
* Generate an CSS file for all style sheets (main and components).
* Returns true if a file was generated, otherwise false.
*/
bool _emitAllCss() {
if (!options.emulateScopedCss) return false;
var buff = new StringBuffer();
// Emit all linked style sheet files first.
for (var file in files) {
var css = new StringBuffer();
var fileInfo = info[file.path];
if (file.isStyleSheet) {
for (var styleSheet in fileInfo.styleSheets) {
css.write(
'/* Auto-generated from style sheet href = ${file.path} */\n'
'/* DO NOT EDIT. */\n\n');
css.write(emitStyleSheet(styleSheet, fileInfo));
css.write('\n\n');
}
}
}
// Emit all CSS for each component (style scoped).
for (var file in files) {
if (file.isHtml) {
var fileInfo = info[file.path];
for (var component in fileInfo.declaredComponents) {
for (var styleSheet in component.styleSheets) {
// Translate any URIs in CSS.
if (buff.isEmpty) {
buff.write(
'/* Auto-generated from components style tags. */\n'
'/* DO NOT EDIT. */\n\n');
}
buff.write(
'/* ==================================================== \n'
' Component ${component.tagName} stylesheet \n'
' ==================================================== */\n');
var tagName = component.tagName;
if (!component.hasAuthorStyles) {
if (_cssResetStyleSheet != null) {
// If component doesn't have apply-author-styles then we need to
// reset the CSS the styles for the component (if css-reset file
// option was passed).
buff.write('\n/* Start CSS Reset */\n');
var style;
if (options.emulateScopedCss) {
style = emitComponentStyleSheet(_cssResetStyleSheet, tagName);
} else {
style = emitOriginalCss(_cssResetStyleSheet);
}
buff.write(style);
buff.write('/* End CSS Reset */\n\n');
}
}
if (options.emulateScopedCss) {
buff.write(emitComponentStyleSheet(styleSheet, tagName));
} else {
buff.write(emitOriginalCss(styleSheet));
}
buff.write('\n\n');
}
}
}
}
if (buff.isEmpty) return false;
return true;
}
_time(String logMessage, String filePath, callback(),
{bool printTime: false}) {
var message = new StringBuffer();
message.write(logMessage);
var filename = path.basename(filePath);
for (int i = (60 - logMessage.length - filename.length); i > 0 ; i--) {
message.write(' ');
}
message.write(filename);
return time(message.toString(), callback,
printTime: options.verbose || printTime);
}
}