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:html/dom.dart' show Document; | |
17 import 'package:html/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, | |
37 encoding: 'utf8', 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 | |
100 ? null | |
101 : entryPoints.map(systemToAssetPath).toList(); | |
102 | |
103 /// Whether an asset with [id] is an entry point HTML file. | |
104 bool isHtmlEntryPoint(AssetId id) { | |
105 if (id.extension != '.html') return false; | |
106 | |
107 // Note: [id.path] is a relative path from the root of a package. | |
108 if (entryPoints == null) { | |
109 return id.path.startsWith('web/') || id.path.startsWith('test/'); | |
110 } | |
111 | |
112 return entryPoints.contains(id.path); | |
113 } | |
114 | |
115 // Whether a stylesheet with [id] should be inlined, the default is true. | |
116 bool shouldInlineStylesheet(AssetId id) { | |
117 // Note: [id.path] is a relative path from the root of a package. | |
118 // Default is to inline everything | |
119 if (inlineStylesheets == null) return true; | |
120 // First check for the full asset path overrides. | |
121 var override = inlineStylesheets[id.toString()]; | |
122 if (override != null) return override; | |
123 // Then check just the path overrides (if the package was not specified). | |
124 override = inlineStylesheets[id.path]; | |
125 if (override != null) return override; | |
126 // Then check the global default setting. | |
127 var globalDefault = inlineStylesheets['default']; | |
128 return (globalDefault != null) ? globalDefault : true; | |
129 } | |
130 | |
131 // Whether a stylesheet with [id] has an overriden inlining setting. | |
132 bool stylesheetInliningIsOverridden(AssetId id) { | |
133 return inlineStylesheets != null && | |
134 (inlineStylesheets.containsKey(id.toString()) || | |
135 inlineStylesheets.containsKey(id.path)); | |
136 } | |
137 } | |
138 | |
139 class LintOptions { | |
140 /// Whether lint is enabled. | |
141 final bool enabled; | |
142 | |
143 /// Patterns explicitly included/excluded from linting (if any). | |
144 final List<RegExp> patterns; | |
145 | |
146 /// When [patterns] is not null, whether they denote inclusion or exclusion. | |
147 final bool isInclude; | |
148 | |
149 const LintOptions() | |
150 : enabled = true, | |
151 patterns = null, | |
152 isInclude = true; | |
153 | |
154 const LintOptions.disabled() | |
155 : enabled = false, | |
156 patterns = null, | |
157 isInclude = true; | |
158 | |
159 LintOptions.include(List<String> patterns) | |
160 : enabled = true, | |
161 isInclude = true, | |
162 patterns = patterns.map((s) => new RegExp(s)).toList(); | |
163 | |
164 LintOptions.exclude(List<String> patterns) | |
165 : enabled = true, | |
166 isInclude = false, | |
167 patterns = patterns.map((s) => new RegExp(s)).toList(); | |
168 | |
169 bool shouldLint(String fileName) { | |
170 if (!enabled) return false; | |
171 if (patterns == null) return isInclude; | |
172 for (var pattern in patterns) { | |
173 if (pattern.hasMatch(fileName)) return isInclude; | |
174 } | |
175 return !isInclude; | |
176 } | |
177 } | |
178 | |
179 /// Mixin for polymer transformers. | |
180 abstract class PolymerTransformer { | |
181 TransformOptions get options; | |
182 | |
183 Future<Document> readPrimaryAsHtml(Transform transform, BuildLogger logger) { | |
184 var asset = transform.primaryInput; | |
185 var id = asset.id; | |
186 return asset.readAsString().then((content) { | |
187 return _parseHtml(content, id.path, logger, | |
188 checkDocType: options.isHtmlEntryPoint(id)); | |
189 }); | |
190 } | |
191 | |
192 Future<Document> readAsHtml( | |
193 AssetId id, Transform transform, BuildLogger logger, | |
194 {bool showWarnings: true}) { | |
195 var primaryId = transform.primaryInput.id; | |
196 bool samePackage = id.package == primaryId.package; | |
197 var url = spanUrlFor(id, transform, logger); | |
198 return transform.readInputAsString(id).then((content) { | |
199 return _parseHtml(content, url, logger, | |
200 checkDocType: samePackage && options.isHtmlEntryPoint(id), | |
201 showWarnings: showWarnings); | |
202 }); | |
203 } | |
204 | |
205 Future<bool> assetExists(AssetId id, Transform transform) => | |
206 transform.getInput(id).then((_) => true).catchError((_) => false); | |
207 | |
208 String toString() => 'polymer ($runtimeType)'; | |
209 } | |
210 | |
211 /// Gets the appropriate URL to use in a span to produce messages (e.g. | |
212 /// warnings) for users. This will attempt to format the URL in the most useful | |
213 /// way: | |
214 /// | |
215 /// - If the asset is within the primary package, then use the [id.path], | |
216 /// the user will know it is a file from their own code. | |
217 /// - If the asset is from another package, then use [assetUrlFor], this will | |
218 /// likely be a "package:" url to the file in the other package, which is | |
219 /// enough for users to identify where the error is. | |
220 String spanUrlFor(AssetId id, Transform transform, logger) { | |
221 var primaryId = transform.primaryInput.id; | |
222 bool samePackage = id.package == primaryId.package; | |
223 return samePackage | |
224 ? id.path | |
225 : assetUrlFor(id, primaryId, logger, allowAssetUrl: true); | |
226 } | |
227 | |
228 /// Transformer phases which should be applied to the Polymer package. | |
229 List<List<Transformer>> get phasesForPolymer => | |
230 [[new ObservableTransformer(files: ['lib/src/instance.dart'])]]; | |
231 | |
232 /// Generate the import url for a file described by [id], referenced by a file | |
233 /// with [sourceId]. | |
234 // TODO(sigmund): this should also be in barback (dartbug.com/12610) | |
235 String assetUrlFor(AssetId id, AssetId sourceId, BuildLogger logger, | |
236 {bool allowAssetUrl: false}) { | |
237 // use package: and asset: urls if possible | |
238 if (id.path.startsWith('lib/')) { | |
239 return 'package:${id.package}/${id.path.substring(4)}'; | |
240 } | |
241 | |
242 if (id.path.startsWith('asset/')) { | |
243 if (!allowAssetUrl) { | |
244 logger.error(INTERNAL_ERROR_DONT_KNOW_HOW_TO_IMPORT.create({ | |
245 'target': id, | |
246 'source': sourceId, | |
247 'extra': ' (asset urls not allowed.)' | |
248 })); | |
249 return null; | |
250 } | |
251 return 'asset:${id.package}/${id.path.substring(6)}'; | |
252 } | |
253 | |
254 // Use relative urls only if it's possible. | |
255 if (id.package != sourceId.package) { | |
256 logger.error("don't know how to refer to $id from $sourceId"); | |
257 return null; | |
258 } | |
259 | |
260 var builder = path.url; | |
261 return builder.relative(builder.join('/', id.path), | |
262 from: builder.join('/', builder.dirname(sourceId.path))); | |
263 } | |
264 | |
265 /// Convert system paths to asset paths (asset paths are posix style). | |
266 String systemToAssetPath(String assetPath) { | |
267 if (path.Style.platform != path.Style.windows) return assetPath; | |
268 return path.posix.joinAll(path.split(assetPath)); | |
269 } | |
270 | |
271 /// Returns true if this is a valid custom element name. See: | |
272 /// <http://w3c.github.io/webcomponents/spec/custom/#dfn-custom-element-type> | |
273 bool isCustomTagName(String name) { | |
274 if (name == null || !name.contains('-')) return false; | |
275 return !invalidTagNames.containsKey(name); | |
276 } | |
277 | |
278 /// Regex to split names in the 'attributes' attribute, which supports 'a b c', | |
279 /// 'a,b,c', or even 'a b,c'. This is the same as in `lib/src/declaration.dart`. | |
280 final ATTRIBUTES_REGEX = new RegExp(r'\s|,'); | |
OLD | NEW |