OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 /// Common methods used by transfomers. |
| 6 library polymer.src.build.common; |
| 7 |
| 8 import 'dart:async'; |
| 9 |
| 10 import 'package:analyzer/src/generated/ast.dart'; |
| 11 import 'package:analyzer/src/generated/error.dart'; |
| 12 import 'package:analyzer/src/generated/parser.dart'; |
| 13 import 'package:analyzer/src/generated/scanner.dart'; |
| 14 import 'package:barback/barback.dart'; |
| 15 import 'package:code_transformers/messages/build_logger.dart'; |
| 16 import 'package:html5lib/dom.dart' show Document; |
| 17 import 'package:html5lib/parser.dart' show HtmlParser; |
| 18 import 'package:observe/transformer.dart' show ObservableTransformer; |
| 19 import 'package:path/path.dart' as path; |
| 20 |
| 21 import 'constants.dart'; |
| 22 import 'messages.dart'; |
| 23 |
| 24 export 'constants.dart'; |
| 25 |
| 26 const _ignoredErrors = const [ |
| 27 'unexpected-dash-after-double-dash-in-comment', |
| 28 'unexpected-char-in-comment', |
| 29 ]; |
| 30 |
| 31 /// Parses an HTML file [contents] and returns a DOM-like tree. Adds emitted |
| 32 /// error/warning to [logger]. |
| 33 Document _parseHtml(String contents, String sourcePath, BuildLogger logger, |
| 34 {bool checkDocType: true, bool showWarnings: true}) { |
| 35 // TODO(jmesserly): make HTTP encoding configurable |
| 36 var parser = new HtmlParser(contents, encoding: 'utf8', |
| 37 generateSpans: true, sourceUrl: sourcePath); |
| 38 var document = parser.parse(); |
| 39 |
| 40 // Note: errors aren't fatal in HTML (unless strict mode is on). |
| 41 // So just print them as warnings. |
| 42 if (showWarnings) { |
| 43 for (var e in parser.errors) { |
| 44 if (_ignoredErrors.contains(e.errorCode)) continue; |
| 45 if (checkDocType || e.errorCode != 'expected-doctype-but-got-start-tag') { |
| 46 logger.warning(HTML5_WARNING.create({'message': e.message}), |
| 47 span: e.span); |
| 48 } |
| 49 } |
| 50 } |
| 51 return document; |
| 52 } |
| 53 |
| 54 /// Additional options used by polymer transformers |
| 55 class TransformOptions { |
| 56 /// List of entrypoints paths. The paths are relative to the package root and |
| 57 /// are represented using posix style, which matches the representation used |
| 58 /// in asset ids in barback. If null, anything under 'web/' or 'test/' is |
| 59 /// considered an entry point. |
| 60 final List<String> entryPoints; |
| 61 |
| 62 /// Map of stylesheet paths that should or should not be inlined. The paths |
| 63 /// are relative to the package root and are represented using posix style, |
| 64 /// which matches the representation used in asset ids in barback. |
| 65 /// |
| 66 /// There is an additional special key 'default' for the global default. |
| 67 final Map<String, bool> inlineStylesheets; |
| 68 |
| 69 /// True to enable Content Security Policy. |
| 70 /// This means the HTML page will not have inlined .js code. |
| 71 final bool contentSecurityPolicy; |
| 72 |
| 73 /// True to include the compiled JavaScript directly from the HTML page. |
| 74 /// If enabled this will remove "packages/browser/dart.js" and replace |
| 75 /// `type="application/dart"` scripts with equivalent *.dart.js files. |
| 76 final bool directlyIncludeJS; |
| 77 |
| 78 /// Run transformers to create a releasable app. For example, include the |
| 79 /// minified versions of the polyfills rather than the debug versions. |
| 80 final bool releaseMode; |
| 81 |
| 82 /// This will make a physical element appear on the page showing build logs. |
| 83 /// It will only appear when ![releaseMode] even if this is true. |
| 84 final bool injectBuildLogsInOutput; |
| 85 |
| 86 /// Rules to determine whether to run liner on an html file. |
| 87 // TODO(jmesserly): instead of this flag, we should only run linter on |
| 88 // reachable (entry point+imported) html if deploying. See dartbug.com/17199. |
| 89 final LintOptions lint; |
| 90 |
| 91 /// This will automatically inject the polyfills from the `web_components` |
| 92 /// package in all entry points, if it is not already included. |
| 93 final bool injectWebComponentsJs; |
| 94 |
| 95 TransformOptions({entryPoints, this.inlineStylesheets, |
| 96 this.contentSecurityPolicy: false, this.directlyIncludeJS: true, |
| 97 this.releaseMode: true, this.lint: const LintOptions(), |
| 98 this.injectBuildLogsInOutput: false, this.injectWebComponentsJs: true}) |
| 99 : entryPoints = entryPoints == null ? null |
| 100 : entryPoints.map(systemToAssetPath).toList(); |
| 101 |
| 102 /// Whether an asset with [id] is an entry point HTML file. |
| 103 bool isHtmlEntryPoint(AssetId id) { |
| 104 if (id.extension != '.html') return false; |
| 105 |
| 106 // Note: [id.path] is a relative path from the root of a package. |
| 107 if (entryPoints == null) { |
| 108 return id.path.startsWith('web/') || id.path.startsWith('test/'); |
| 109 } |
| 110 |
| 111 return entryPoints.contains(id.path); |
| 112 } |
| 113 |
| 114 // Whether a stylesheet with [id] should be inlined, the default is true. |
| 115 bool shouldInlineStylesheet(AssetId id) { |
| 116 // Note: [id.path] is a relative path from the root of a package. |
| 117 // Default is to inline everything |
| 118 if (inlineStylesheets == null) return true; |
| 119 // First check for the full asset path overrides. |
| 120 var override = inlineStylesheets[id.toString()]; |
| 121 if (override != null) return override; |
| 122 // Then check just the path overrides (if the package was not specified). |
| 123 override = inlineStylesheets[id.path]; |
| 124 if (override != null) return override; |
| 125 // Then check the global default setting. |
| 126 var globalDefault = inlineStylesheets['default']; |
| 127 return (globalDefault != null) ? globalDefault : true; |
| 128 } |
| 129 |
| 130 // Whether a stylesheet with [id] has an overriden inlining setting. |
| 131 bool stylesheetInliningIsOverridden(AssetId id) { |
| 132 return inlineStylesheets != null && |
| 133 (inlineStylesheets.containsKey(id.toString()) |
| 134 || inlineStylesheets.containsKey(id.path)); |
| 135 } |
| 136 } |
| 137 |
| 138 class LintOptions { |
| 139 /// Whether lint is enabled. |
| 140 final bool enabled; |
| 141 |
| 142 /// Patterns explicitly included/excluded from linting (if any). |
| 143 final List<RegExp> patterns; |
| 144 |
| 145 /// When [patterns] is not null, whether they denote inclusion or exclusion. |
| 146 final bool isInclude; |
| 147 |
| 148 const LintOptions() |
| 149 : enabled = true, patterns = null, isInclude = true; |
| 150 const LintOptions.disabled() |
| 151 : enabled = false, patterns = null, isInclude = true; |
| 152 |
| 153 LintOptions.include(List<String> patterns) |
| 154 : enabled = true, |
| 155 isInclude = true, |
| 156 patterns = patterns.map((s) => new RegExp(s)).toList(); |
| 157 |
| 158 LintOptions.exclude(List<String> patterns) |
| 159 : enabled = true, |
| 160 isInclude = false, |
| 161 patterns = patterns.map((s) => new RegExp(s)).toList(); |
| 162 |
| 163 bool shouldLint(String fileName) { |
| 164 if (!enabled) return false; |
| 165 if (patterns == null) return isInclude; |
| 166 for (var pattern in patterns) { |
| 167 if (pattern.hasMatch(fileName)) return isInclude; |
| 168 } |
| 169 return !isInclude; |
| 170 } |
| 171 } |
| 172 |
| 173 /// Mixin for polymer transformers. |
| 174 abstract class PolymerTransformer { |
| 175 TransformOptions get options; |
| 176 |
| 177 Future<Document> readPrimaryAsHtml(Transform transform, BuildLogger logger) { |
| 178 var asset = transform.primaryInput; |
| 179 var id = asset.id; |
| 180 return asset.readAsString().then((content) { |
| 181 return _parseHtml(content, id.path, logger, |
| 182 checkDocType: options.isHtmlEntryPoint(id)); |
| 183 }); |
| 184 } |
| 185 |
| 186 Future<Document> readAsHtml(AssetId id, Transform transform, |
| 187 BuildLogger logger, |
| 188 {bool showWarnings: true}) { |
| 189 var primaryId = transform.primaryInput.id; |
| 190 bool samePackage = id.package == primaryId.package; |
| 191 var url = spanUrlFor(id, transform, logger); |
| 192 return transform.readInputAsString(id).then((content) { |
| 193 return _parseHtml(content, url, logger, |
| 194 checkDocType: samePackage && options.isHtmlEntryPoint(id), |
| 195 showWarnings: showWarnings); |
| 196 }); |
| 197 } |
| 198 |
| 199 Future<bool> assetExists(AssetId id, Transform transform) => |
| 200 transform.getInput(id).then((_) => true).catchError((_) => false); |
| 201 |
| 202 String toString() => 'polymer ($runtimeType)'; |
| 203 } |
| 204 |
| 205 /// Gets the appropriate URL to use in a span to produce messages (e.g. |
| 206 /// warnings) for users. This will attempt to format the URL in the most useful |
| 207 /// way: |
| 208 /// |
| 209 /// - If the asset is within the primary package, then use the [id.path], |
| 210 /// the user will know it is a file from their own code. |
| 211 /// - If the asset is from another package, then use [assetUrlFor], this will |
| 212 /// likely be a "package:" url to the file in the other package, which is |
| 213 /// enough for users to identify where the error is. |
| 214 String spanUrlFor(AssetId id, Transform transform, logger) { |
| 215 var primaryId = transform.primaryInput.id; |
| 216 bool samePackage = id.package == primaryId.package; |
| 217 return samePackage ? id.path |
| 218 : assetUrlFor(id, primaryId, logger, allowAssetUrl: true); |
| 219 } |
| 220 |
| 221 /// Transformer phases which should be applied to the Polymer package. |
| 222 List<List<Transformer>> get phasesForPolymer => |
| 223 [[new ObservableTransformer(['lib/src/instance.dart'])]]; |
| 224 |
| 225 /// Generate the import url for a file described by [id], referenced by a file |
| 226 /// with [sourceId]. |
| 227 // TODO(sigmund): this should also be in barback (dartbug.com/12610) |
| 228 String assetUrlFor(AssetId id, AssetId sourceId, BuildLogger logger, |
| 229 {bool allowAssetUrl: false}) { |
| 230 // use package: and asset: urls if possible |
| 231 if (id.path.startsWith('lib/')) { |
| 232 return 'package:${id.package}/${id.path.substring(4)}'; |
| 233 } |
| 234 |
| 235 if (id.path.startsWith('asset/')) { |
| 236 if (!allowAssetUrl) { |
| 237 logger.error(INTERNAL_ERROR_DONT_KNOW_HOW_TO_IMPORT.create({ |
| 238 'target': id, |
| 239 'source': sourceId, |
| 240 'extra': ' (asset urls not allowed.)'})); |
| 241 return null; |
| 242 } |
| 243 return 'asset:${id.package}/${id.path.substring(6)}'; |
| 244 } |
| 245 |
| 246 // Use relative urls only if it's possible. |
| 247 if (id.package != sourceId.package) { |
| 248 logger.error("don't know how to refer to $id from $sourceId"); |
| 249 return null; |
| 250 } |
| 251 |
| 252 var builder = path.url; |
| 253 return builder.relative(builder.join('/', id.path), |
| 254 from: builder.join('/', builder.dirname(sourceId.path))); |
| 255 } |
| 256 |
| 257 |
| 258 /// Convert system paths to asset paths (asset paths are posix style). |
| 259 String systemToAssetPath(String assetPath) { |
| 260 if (path.Style.platform != path.Style.windows) return assetPath; |
| 261 return path.posix.joinAll(path.split(assetPath)); |
| 262 } |
| 263 |
| 264 /// Returns true if this is a valid custom element name. See: |
| 265 /// <http://w3c.github.io/webcomponents/spec/custom/#dfn-custom-element-type> |
| 266 bool isCustomTagName(String name) { |
| 267 if (name == null || !name.contains('-')) return false; |
| 268 return !invalidTagNames.containsKey(name); |
| 269 } |
| 270 |
| 271 /// Regex to split names in the 'attributes' attribute, which supports 'a b c', |
| 272 /// 'a,b,c', or even 'a b,c'. This is the same as in `lib/src/declaration.dart`. |
| 273 final ATTRIBUTES_REGEX = new RegExp(r'\s|,'); |
OLD | NEW |