OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012, 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 /** | |
6 * This generates the reference documentation for the core libraries that come | |
7 * with dart. It is built on top of dartdoc, which is a general-purpose library | |
8 * for generating docs from any Dart code. This library extends that to include | |
9 * additional information and styling specific to our standard library. | |
10 * | |
11 * Usage: | |
12 * | |
13 * $ dart apidoc.dart [--out=<output directory>] | |
14 */ | |
15 library apidoc; | |
16 | |
17 import 'dart:async'; | |
18 import 'dart:convert'; | |
19 import 'dart:io'; | |
20 | |
21 import 'html_diff.dart'; | |
22 | |
23 import 'package:compiler/src/mirrors/source_mirrors.dart'; | |
24 import 'package:compiler/src/mirrors/mirrors_util.dart'; | |
25 import 'package:compiler/src/filenames.dart'; | |
26 import 'package:dartdoc/dartdoc.dart'; | |
27 import 'package:sdk_library_metadata/libraries.dart'; | |
28 import 'package:path/path.dart' as path; | |
29 | |
30 HtmlDiff _diff; | |
31 | |
32 void main(List<String> args) { | |
33 int mode = MODE_STATIC; | |
34 String outputDir = 'docs'; | |
35 bool generateAppCache = false; | |
36 | |
37 List<String> excludedLibraries = <String>[]; | |
38 | |
39 // For libraries that have names matching the package name, | |
40 // such as library unittest in package unittest, we just give | |
41 // the package name with a --include-lib argument, such as: | |
42 // --include-lib=unittest. These arguments are collected in | |
43 // includedLibraries. | |
44 List<String> includedLibraries = <String>[]; | |
45 | |
46 // For libraries that lie within packages but have a different name, | |
47 // such as the matcher library in package unittest, we can use | |
48 // --extra-lib with a full relative path under pkg, such as | |
49 // --extra-lib=unittest/lib/matcher.dart. These arguments are | |
50 // collected in extraLibraries. | |
51 List<String> extraLibraries = <String>[]; | |
52 | |
53 String packageRoot; | |
54 String version; | |
55 | |
56 // Parse the command-line arguments. | |
57 for (int i = 0; i < args.length; i++) { | |
58 final arg = args[i]; | |
59 | |
60 switch (arg) { | |
61 case '--mode=static': | |
62 mode = MODE_STATIC; | |
63 break; | |
64 | |
65 case '--mode=live-nav': | |
66 mode = MODE_LIVE_NAV; | |
67 break; | |
68 | |
69 case '--generate-app-cache=true': | |
70 generateAppCache = true; | |
71 break; | |
72 | |
73 default: | |
74 if (arg.startsWith('--exclude-lib=')) { | |
75 excludedLibraries.add(arg.substring('--exclude-lib='.length)); | |
76 } else if (arg.startsWith('--include-lib=')) { | |
77 includedLibraries.add(arg.substring('--include-lib='.length)); | |
78 } else if (arg.startsWith('--extra-lib=')) { | |
79 extraLibraries.add(arg.substring('--extra-lib='.length)); | |
80 } else if (arg.startsWith('--out=')) { | |
81 outputDir = arg.substring('--out='.length); | |
82 } else if (arg.startsWith('--package-root=')) { | |
83 packageRoot = arg.substring('--package-root='.length); | |
84 } else if (arg.startsWith('--version=')) { | |
85 version = arg.substring('--version='.length); | |
86 } else { | |
87 print('Unknown option: $arg'); | |
88 return; | |
89 } | |
90 break; | |
91 } | |
92 } | |
93 | |
94 final libPath = path.join(scriptDir, '..', '..', 'sdk/'); | |
95 | |
96 cleanOutputDirectory(outputDir); | |
97 | |
98 print('Copying static files...'); | |
99 // The basic dartdoc-provided static content. | |
100 final copiedStatic = copyDirectory( | |
101 path.join(scriptDir, | |
102 '..', '..', 'sdk', 'lib', '_internal', 'dartdoc', 'static'), | |
103 outputDir); | |
104 | |
105 // The apidoc-specific static content. | |
106 final copiedApiDocStatic = copyDirectory( | |
107 path.join(scriptDir, 'static'), | |
108 outputDir); | |
109 | |
110 print('Parsing MDN data...'); | |
111 final mdnFile = new File(path.join(scriptDir, 'mdn', 'database.json')); | |
112 final mdn = JSON.decode(mdnFile.readAsStringSync()); | |
113 | |
114 print('Cross-referencing dart:html...'); | |
115 // TODO(amouravski): move HtmlDiff inside of the future chain below to re-use | |
116 // the MirrorSystem already analyzed. | |
117 _diff = new HtmlDiff(printWarnings:false); | |
118 Future htmlDiff = _diff.run(currentDirectory.resolveUri(path.toUri(libPath))); | |
119 | |
120 // TODO(johnniwinther): Libraries for the compilation seem to be more like | |
121 // URIs. Perhaps Path should have a toURI() method. | |
122 // Add all of the core libraries. | |
123 final apidocLibraries = <Uri>[]; | |
124 LIBRARIES.forEach((String name, LibraryInfo info) { | |
125 if (info.documented) { | |
126 apidocLibraries.add(Uri.parse('dart:$name')); | |
127 } | |
128 }); | |
129 | |
130 // TODO(amouravski): This code is really wonky. | |
131 var lister = new Directory(path.join(scriptDir, '..', '..', 'pkg')).list(); | |
132 lister.listen((entity) { | |
133 if (entity is Directory) { | |
134 var libName = path.basename(entity.path); | |
135 var libPath = path.join(entity.path, 'lib', '${libName}.dart'); | |
136 | |
137 // Ignore some libraries. | |
138 if (excludedLibraries.contains(libName)) { | |
139 return; | |
140 } | |
141 | |
142 // Ignore hidden directories (like .svn) as well as pkg.xcodeproj. | |
143 if (libName.startsWith('.') || libName.endsWith('.xcodeproj')) { | |
144 return; | |
145 } | |
146 | |
147 if (new File(libPath).existsSync()) { | |
148 apidocLibraries.add(path.toUri(libPath)); | |
149 includedLibraries.add(libName); | |
150 } else { | |
151 print('Warning: could not find package at ${entity.path}'); | |
152 } | |
153 } | |
154 }, onDone: () { | |
155 // Add any --extra libraries that had full pkg paths. | |
156 // TODO(gram): if the handling of --include-lib libraries in the | |
157 // listen() block above is cleaned up, then this will need to be | |
158 // too, as it is a special case of the above. | |
159 for (var lib in extraLibraries) { | |
160 var libPath = '../../$lib'; | |
161 if (new File(libPath).existsSync()) { | |
162 apidocLibraries.add(path.toUri(libPath)); | |
163 var libName = path.basename(libPath).replaceAll('.dart', ''); | |
164 includedLibraries.add(libName); | |
165 } | |
166 } | |
167 | |
168 final apidoc = new Apidoc(mdn, outputDir, mode, generateAppCache, | |
169 excludedLibraries, version); | |
170 apidoc.dartdocPath = | |
171 path.join(scriptDir, '..', '..', 'sdk', 'lib', '_internal', 'dartdoc'); | |
172 // Select the libraries to include in the produced documentation: | |
173 apidoc.includeApi = true; | |
174 apidoc.includedLibraries = includedLibraries; | |
175 | |
176 // TODO(amouravski): make apidoc use roughly the same flow as bin/dartdoc. | |
177 Future.wait([copiedStatic, copiedApiDocStatic, htmlDiff]) | |
178 .then((_) => apidoc.documentLibraries(apidocLibraries, libPath, | |
179 packageRoot)) | |
180 .then((_) => compileScript(mode, outputDir, libPath, apidoc.tmpPath)) | |
181 .then((_) => print(apidoc.status)) | |
182 .catchError((e, trace) { | |
183 print('Error: generation failed: ${e}'); | |
184 if (trace != null) print("StackTrace: $trace"); | |
185 apidoc.cleanup(); | |
186 exit(1); | |
187 }) | |
188 .whenComplete(() => apidoc.cleanup()); | |
189 }); | |
190 } | |
191 | |
192 class Apidoc extends Dartdoc { | |
193 /** Big ball of JSON containing the scraped MDN documentation. */ | |
194 final Map mdn; | |
195 | |
196 | |
197 // A set of type names (TypeMirror.simpleName values) to ignore while | |
198 // looking up information from MDN data. TODO(eub, jacobr): fix up the MDN | |
199 // import scripts so they run correctly and generate data that doesn't have | |
200 // any entries that need to be ignored. | |
201 static Set<String> _mdnTypeNamesToSkip = null; | |
202 | |
203 /** | |
204 * The URL to the page on MDN that content was pulled from for the current | |
205 * type being documented. Will be `null` if the type doesn't use any MDN | |
206 * content. | |
207 */ | |
208 String mdnUrl = null; | |
209 | |
210 Apidoc(this.mdn, String outputDir, int mode, bool generateAppCache, | |
211 [List<String> excludedLibraries, String version]) { | |
212 if (excludedLibraries != null) this.excludedLibraries = excludedLibraries; | |
213 this.version = version; | |
214 this.outputDir = outputDir; | |
215 this.mode = mode; | |
216 this.generateAppCache = generateAppCache; | |
217 | |
218 // Skip bad entries in the checked-in mdn/database.json: | |
219 // * UnknownElement has a top-level Gecko DOM page in German. | |
220 if (_mdnTypeNamesToSkip == null) | |
221 _mdnTypeNamesToSkip = new Set.from(['UnknownElement']); | |
222 | |
223 mainTitle = 'Dart API Reference'; | |
224 mainUrl = 'http://dartlang.org'; | |
225 | |
226 final note = 'http://code.google.com/policies.html#restrictions'; | |
227 final cca = 'http://creativecommons.org/licenses/by/3.0/'; | |
228 final bsd = 'http://code.google.com/google_bsd_license.html'; | |
229 final tos = 'http://www.dartlang.org/tos.html'; | |
230 final privacy = 'http://www.google.com/intl/en/privacy/privacy-policy.html'; | |
231 | |
232 footerText = | |
233 ''' | |
234 <p>Except as otherwise <a href="$note">noted</a>, the content of this | |
235 page is licensed under the <a href="$cca">Creative Commons Attribution | |
236 3.0 License</a>, and code samples are licensed under the | |
237 <a href="$bsd">BSD License</a>.</p> | |
238 <p><a href="$tos">Terms of Service</a> | | |
239 <a href="$privacy">Privacy Policy</a></p> | |
240 '''; | |
241 | |
242 searchEngineId = '011220921317074318178:i4mscbaxtru'; | |
243 searchResultsUrl = 'http://www.dartlang.org/search.html'; | |
244 } | |
245 | |
246 void writeHeadContents(String title) { | |
247 super.writeHeadContents(title); | |
248 | |
249 // Include the apidoc-specific CSS. | |
250 // TODO(rnystrom): Use our CSS pre-processor to combine these. | |
251 writeln( | |
252 ''' | |
253 <link rel="stylesheet" type="text/css" | |
254 href="${relativePath('apidoc-styles.css')}" /> | |
255 '''); | |
256 | |
257 // Add the analytics code. | |
258 writeln( | |
259 ''' | |
260 <script type="text/javascript"> | |
261 var _gaq = _gaq || []; | |
262 _gaq.push(["_setAccount", "UA-26406144-9"]); | |
263 _gaq.push(["_trackPageview"]); | |
264 | |
265 (function() { | |
266 var ga = document.createElement("script"); | |
267 ga.type = "text/javascript"; ga.async = true; | |
268 ga.src = ("https:" == document.location.protocol ? | |
269 "https://ssl" : "http://www") + ".google-analytics.com/ga.js"; | |
270 var s = document.getElementsByTagName("script")[0]; | |
271 s.parentNode.insertBefore(ga, s); | |
272 })(); | |
273 </script> | |
274 '''); | |
275 } | |
276 | |
277 void docIndexLibrary(LibraryMirror library) { | |
278 // TODO(rnystrom): Hackish. The IO libraries reference this but we don't | |
279 // want it in the docs. | |
280 if (displayName(library) == 'dart:nativewrappers') return; | |
281 super.docIndexLibrary(library); | |
282 } | |
283 | |
284 void docLibraryNavigationJson(LibraryMirror library, List libraryList) { | |
285 // TODO(rnystrom): Hackish. The IO libraries reference this but we don't | |
286 // want it in the docs. | |
287 if (displayName(library) == 'dart:nativewrappers') return; | |
288 super.docLibraryNavigationJson(library, libraryList); | |
289 } | |
290 | |
291 void docLibrary(LibraryMirror library) { | |
292 // TODO(rnystrom): Hackish. The IO libraries reference this but we don't | |
293 // want it in the docs. | |
294 if (displayName(library) == 'dart:nativewrappers') return; | |
295 super.docLibrary(library); | |
296 } | |
297 | |
298 DocComment getLibraryComment(LibraryMirror library) { | |
299 return super.getLibraryComment(library); | |
300 } | |
301 | |
302 DocComment getTypeComment(TypeMirror type) { | |
303 return _mergeDocs( | |
304 includeMdnTypeComment(type), super.getTypeComment(type)); | |
305 } | |
306 | |
307 DocComment getMemberComment(DeclarationMirror member) { | |
308 return _mergeDocs( | |
309 includeMdnMemberComment(member), super.getMemberComment(member)); | |
310 } | |
311 | |
312 DocComment _mergeDocs(MdnComment mdnComment, | |
313 DocComment fileComment) { | |
314 // Otherwise, prefer comment from the (possibly generated) Dart file. | |
315 if (fileComment != null) return fileComment; | |
316 | |
317 // Finally, fallback on MDN if available. | |
318 if (mdnComment != null) { | |
319 mdnUrl = mdnComment.mdnUrl; | |
320 return mdnComment; | |
321 } | |
322 | |
323 // We got nothing! | |
324 return null; | |
325 } | |
326 | |
327 void docType(TypeMirror type) { | |
328 // Track whether we've inserted MDN content into this page. | |
329 mdnUrl = null; | |
330 | |
331 super.docType(type); | |
332 } | |
333 | |
334 void writeTypeFooter() { | |
335 if (mdnUrl != null) { | |
336 final MOZ = 'http://www.mozilla.org/'; | |
337 final MDN = 'https://developer.mozilla.org'; | |
338 final CCA = 'http://creativecommons.org/licenses/by-sa/2.5/'; | |
339 final CONTRIB = 'https://developer.mozilla.org/Project:en/How_to_Help'; | |
340 writeln( | |
341 ''' | |
342 <p class="mdn-attribution"> | |
343 <a href="$MDN"> | |
344 <img src="${relativePath('mdn-logo-tiny.png')}" class="mdn-logo" /> | |
345 </a> | |
346 This page includes <a href="$mdnUrl">content</a> from the | |
347 <a href="$MOZ">Mozilla Foundation</a> that is graciously | |
348 <a href="$MDN/Project:Copyrights">licensed</a> under a | |
349 <a href="$CCA">Creative Commons: Attribution-Sharealike license</a>. | |
350 Mozilla has no other association with Dart or dartlang.org. We | |
351 encourage you to improve the web by | |
352 <a href="$CONTRIB">contributing</a> to | |
353 <a href="$MDN">The Mozilla Developer Network</a>. | |
354 </p> | |
355 '''); | |
356 } | |
357 } | |
358 | |
359 MdnComment lookupMdnComment(Mirror mirror) { | |
360 if (mirror is TypeMirror) { | |
361 return includeMdnTypeComment(mirror); | |
362 } else if (mirror is MethodMirror || mirror is VariableMirror) { | |
363 return includeMdnMemberComment(mirror); | |
364 } else { | |
365 return null; | |
366 } | |
367 } | |
368 | |
369 /** | |
370 * Gets the MDN-scraped docs for [type], or `null` if this type isn't | |
371 * scraped from MDN. | |
372 */ | |
373 MdnComment includeMdnTypeComment(TypeMirror type) { | |
374 if (_mdnTypeNamesToSkip.contains(type.simpleName)) { | |
375 return null; | |
376 } | |
377 | |
378 var typeString = ''; | |
379 if (HTML_LIBRARY_URIS.contains(getLibrary(type).uri)) { | |
380 // If it's an HTML type, try to map it to a base DOM type so we can find | |
381 // the MDN docs. | |
382 final domTypes = _diff.htmlTypesToDom[type.qualifiedName]; | |
383 | |
384 // Couldn't find a DOM type. | |
385 if ((domTypes == null) || (domTypes.length != 1)) return null; | |
386 | |
387 // Use the corresponding DOM type when searching MDN. | |
388 // TODO(rnystrom): Shame there isn't a simpler way to get the one item | |
389 // out of a singleton Set. | |
390 // TODO(floitsch): switch to domTypes.first, once that's implemented. | |
391 var iter = domTypes.iterator; | |
392 iter.moveNext(); | |
393 typeString = iter.current; | |
394 } else { | |
395 // Not a DOM type. | |
396 return null; | |
397 } | |
398 | |
399 final mdnType = mdn[typeString]; | |
400 if (mdnType == null) return null; | |
401 if (mdnType['skipped'] != null) return null; | |
402 if (mdnType['summary'] == null) return null; | |
403 if (mdnType['summary'].trim().isEmpty) return null; | |
404 | |
405 // Remember which MDN page we're using so we can attribute it. | |
406 return new MdnComment(mdnType['summary'], mdnType['srcUrl']); | |
407 } | |
408 | |
409 /** | |
410 * Gets the MDN-scraped docs for [member], or `null` if this type isn't | |
411 * scraped from MDN. | |
412 */ | |
413 MdnComment includeMdnMemberComment(DeclarationMirror member) { | |
414 var library = getLibrary(member); | |
415 var memberString = ''; | |
416 if (HTML_LIBRARY_URIS.contains(library.uri)) { | |
417 // If it's an HTML type, try to map it to a DOM type name so we can find | |
418 // the MDN docs. | |
419 final domMembers = _diff.htmlToDom[member.qualifiedName]; | |
420 | |
421 // Couldn't find a DOM type. | |
422 if ((domMembers == null) || (domMembers.length != 1)) return null; | |
423 | |
424 // Use the corresponding DOM member when searching MDN. | |
425 // TODO(rnystrom): Shame there isn't a simpler way to get the one item | |
426 // out of a singleton Set. | |
427 // TODO(floitsch): switch to domTypes.first, once that's implemented. | |
428 var iter = domMembers.iterator; | |
429 iter.moveNext(); | |
430 memberString = iter.current; | |
431 } else { | |
432 // Not a DOM type. | |
433 return null; | |
434 } | |
435 | |
436 // Ignore top-level functions. | |
437 if (member.isTopLevel) return null; | |
438 | |
439 var mdnMember = null; | |
440 var mdnType = null; | |
441 var pieces = memberString.split('.'); | |
442 if (pieces.length == 2) { | |
443 mdnType = mdn[pieces[0]]; | |
444 if (mdnType == null) return null; | |
445 var nameToFind = pieces[1]; | |
446 for (final candidateMember in mdnType['members']) { | |
447 if (candidateMember['name'] == nameToFind) { | |
448 mdnMember = candidateMember; | |
449 break; | |
450 } | |
451 } | |
452 } | |
453 | |
454 if (mdnMember == null) return null; | |
455 if (mdnMember['help'] == null) return null; | |
456 if (mdnMember['help'].trim().isEmpty) return null; | |
457 | |
458 // Remember which MDN page we're using so we can attribute it. | |
459 return new MdnComment(mdnMember['help'], mdnType['srcUrl']); | |
460 } | |
461 | |
462 /** | |
463 * Returns a link to [member], relative to a type page that may be in a | |
464 * different library than [member]. | |
465 */ | |
466 String _linkMember(DeclarationMirror member) { | |
467 final typeName = member.owner.simpleName; | |
468 var memberName = '$typeName.${member.simpleName}'; | |
469 if (member is MethodMirror && member.isConstructor) { | |
470 final separator = member.constructorName == '' ? '' : '.'; | |
471 memberName = 'new $typeName$separator${member.constructorName}'; | |
472 } | |
473 | |
474 return a(memberUrl(member), memberName); | |
475 } | |
476 } | |
477 | |
OLD | NEW |