| // Copyright (c) 2019, 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:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/file_system/file_system.dart'; |
| import 'package:dartdoc/src/comment_references/model_comment_reference.dart'; |
| import 'package:dartdoc/src/dartdoc_options.dart'; |
| import 'package:dartdoc/src/io_utils.dart'; |
| import 'package:dartdoc/src/model/comment_referable.dart'; |
| import 'package:dartdoc/src/model/model.dart'; |
| import 'package:dartdoc/src/model/model_object_builder.dart'; |
| import 'package:dartdoc/src/package_meta.dart'; |
| import 'package:dartdoc/src/warnings.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:path/path.dart' as path show Context; |
| import 'package:pub_semver/pub_semver.dart'; |
| |
| // All hrefs are emitted as relative paths from the output root. We are unable |
| // to compute them from the page we are generating, and many properties computed |
| // using hrefs are memoized anyway. To build complete relative hrefs, we emit |
| // the href with this placeholder, and then replace it with the current page's |
| // base href afterwards. |
| // See https://github.com/dart-lang/dartdoc/issues/2090 for further context. |
| // TODO: Find an approach that doesn't require doing this. |
| // Unlikely to be mistaken for an identifier, html tag, or something else that |
| // might reasonably exist normally. |
| @internal |
| const String htmlBasePlaceholder = r'%%__HTMLBASE_dartdoc_internal__%%'; |
| |
| /// A [LibraryContainer] that contains [Library] objects related to a particular |
| /// package. |
| class Package extends LibraryContainer |
| with |
| Nameable, |
| Locatable, |
| Canonicalization, |
| Warnable, |
| CommentReferable, |
| ModelBuilder |
| implements Privacy, Documentable { |
| String _name; |
| PackageGraph _packageGraph; |
| |
| final Map<String, Category> _nameToCategory = {}; |
| |
| // Creates a package, if necessary, and adds it to the [packageGraph]. |
| factory Package.fromPackageMeta( |
| PackageMeta packageMeta, PackageGraph packageGraph) { |
| var packageName = packageMeta.name; |
| |
| var expectNonLocal = false; |
| |
| if (!packageGraph.packageMap.containsKey(packageName) && |
| packageGraph.allLibrariesAdded) expectNonLocal = true; |
| packageGraph.packageMap.putIfAbsent( |
| packageName, () => Package._(packageName, packageGraph, packageMeta)); |
| // Verify that we don't somehow decide to document locally a package picked |
| // up after all documented libraries are added, because that breaks the |
| // assumption that we've picked up all documented libraries and packages |
| // before allLibrariesAdded is true. |
| assert( |
| !(expectNonLocal && |
| packageGraph.packageMap[packageName].documentedWhere == |
| DocumentLocation.local), |
| 'Found more libraries to document after allLibrariesAdded was set to true'); |
| return packageGraph.packageMap[packageName]; |
| } |
| |
| Package._(this._name, this._packageGraph, this._packageMeta); |
| |
| @override |
| bool get isCanonical => true; |
| |
| @override |
| Library get canonicalLibrary => null; |
| |
| /// Number of times we have invoked a tool for this package. |
| int toolInvocationIndex = 0; |
| |
| // The animation IDs that have already been used, indexed by the [href] of the |
| // object that contains them. |
| Map<String, Set<String>> usedAnimationIdsByHref = {}; |
| |
| /// Pieces of the location, split to remove 'package:' and slashes. |
| @override |
| Set<String> get locationPieces => {}; |
| |
| /// Holds all libraries added to this package. May include non-documented |
| /// libraries, but is not guaranteed to include a complete list of |
| /// non-documented libraries unless they are all referenced by documented ones. |
| final Set<Library> allLibraries = {}; |
| |
| bool get hasHomepage => |
| packageMeta.homepage != null && packageMeta.homepage.isNotEmpty; |
| |
| String get homepage => packageMeta.homepage; |
| |
| @override |
| String get kind => (isSdk) ? 'SDK' : 'package'; |
| |
| @override |
| List<Locatable> get documentationFrom => [this]; |
| |
| /// Return true if the code has defined non-default categories for libraries |
| /// in this package. |
| bool get hasCategories => categories.isNotEmpty; |
| |
| LibraryContainer get defaultCategory => nameToCategory[null]; |
| |
| String _documentationAsHtml; |
| |
| @override |
| String get documentationAsHtml { |
| if (_documentationAsHtml != null) return _documentationAsHtml; |
| _documentationAsHtml = Documentation.forElement(this).asHtml; |
| |
| return _documentationAsHtml; |
| } |
| |
| String /*?*/ _documentation; |
| |
| @override |
| String get documentation { |
| if (_documentation == null) { |
| final docFile = documentationFile; |
| if (docFile != null) { |
| _documentation = packageGraph.resourceProvider |
| .readAsMalformedAllowedStringSync(docFile); |
| } |
| } |
| return _documentation; |
| } |
| |
| @override |
| bool get hasDocumentation => documentation?.isNotEmpty == true; |
| |
| @override |
| bool get hasExtendedDocumentation => hasDocumentation; |
| |
| File /*?*/ _documentationFile; |
| |
| File /*?*/ get documentationFile => |
| _documentationFile ??= packageMeta.getReadmeContents(); |
| |
| @override |
| String get oneLineDoc => ''; |
| |
| @override |
| bool get isDocumented => |
| isFirstPackage || documentedWhere != DocumentLocation.missing; |
| |
| @override |
| Warnable get enclosingElement => null; |
| |
| bool _isPublic; |
| |
| @override |
| bool get isPublic { |
| _isPublic ??= libraries.any((l) => l.isPublic); |
| return _isPublic; |
| } |
| |
| bool _isLocal; |
| |
| /// Return true if this is the default package, this is part of an embedder |
| /// SDK, or if [DartdocOptionContext.autoIncludeDependencies] is true -- but |
| /// only if the package was not excluded on the command line. |
| bool get isLocal { |
| _isLocal ??= ( |
| // Document as local if this is the default package. |
| packageMeta == packageGraph.packageMeta || |
| // Assume we want to document an embedded SDK as local if |
| // it has libraries defined in the default package. |
| // TODO(jcollins-g): Handle case where embedder SDKs can be |
| // assembled from multiple locations? |
| packageGraph.hasEmbedderSdk && |
| packageMeta.isSdk && |
| libraries.any((l) => _pathContext.isWithin( |
| packageGraph.packageMeta.dir.path, |
| (l.element.source.fullName))) || |
| // autoIncludeDependencies means everything is local. |
| packageGraph.config.autoIncludeDependencies) && |
| // Regardless of the above rules, do not document as local if |
| // we excluded this package by name. |
| !packageGraph.config.isPackageExcluded(name); |
| return _isLocal; |
| } |
| |
| /* late */ DocumentLocation _documentedWhere; |
| |
| DocumentLocation get documentedWhere { |
| if (_documentedWhere == null) { |
| if (isLocal) { |
| if (isPublic) { |
| _documentedWhere = DocumentLocation.local; |
| } |
| } else { |
| if (config.linkToRemote && |
| config.linkToUrl.isNotEmpty && |
| isPublic && |
| !packageGraph.config.isPackageExcluded(name)) { |
| _documentedWhere = DocumentLocation.remote; |
| } else { |
| _documentedWhere = DocumentLocation.missing; |
| } |
| } |
| } |
| return _documentedWhere; |
| } |
| |
| @override |
| String get enclosingName => packageGraph.defaultPackageName; |
| |
| String get filePath => 'index.$fileType'; |
| |
| String _fileType; |
| |
| String get fileType { |
| // TODO(jdkoren): Provide a way to determine file type of a remote package's |
| // docs. Perhaps make this configurable through dartdoc options. |
| // In theory, a remote package could be documented in any supported format. |
| // In practice, devs depend on Dart, Flutter, and/or packages fetched |
| // from pub.dev, and we know that all of those use html docs. |
| return _fileType ??= (package.documentedWhere == DocumentLocation.remote) |
| ? 'html' |
| : config.format; |
| } |
| |
| @override |
| String get fullyQualifiedName => 'package:$name'; |
| |
| String _baseHref; |
| |
| String get baseHref { |
| if (_baseHref != null) { |
| return _baseHref; |
| } |
| |
| if (documentedWhere == DocumentLocation.remote) { |
| _baseHref = _remoteBaseHref; |
| if (!_baseHref.endsWith('/')) _baseHref = '$_baseHref/'; |
| } else { |
| _baseHref = config.useBaseHref ? '' : htmlBasePlaceholder; |
| } |
| |
| return _baseHref; |
| } |
| |
| String get _remoteBaseHref { |
| return config.linkToUrl.replaceAllMapped(_substituteNameVersion, (m) { |
| switch (m.group(1)) { |
| // Return the prerelease tag of the release if a prerelease, or 'stable' |
| // otherwise. Mostly coded around the Dart SDK's use of dev/stable, but |
| // theoretically applicable elsewhere. |
| case 'b': |
| { |
| var version = Version.parse(packageMeta.version); |
| var tag = 'stable'; |
| if (version.isPreRelease) { |
| // `version.preRelease` is a `List<dynamic>` with a mix of |
| // integers and strings. Given this, handle |
| // "2.8.0-dev.1.0, 2.9.0-1.0.dev", and similar variations. |
| tag = version.preRelease.whereType<String>().first; |
| // Who knows about non-SDK packages, but SDKs must conform to the |
| // known format. |
| assert(packageMeta.isSdk == false || int.tryParse(tag) == null, |
| 'Got an integer as string instead of the expected "dev" tag'); |
| } |
| return tag; |
| } |
| case 'n': |
| return name; |
| // The full version string of the package. |
| case 'v': |
| return packageMeta.version; |
| default: |
| assert(false, 'Unsupported case: ${m.group(1)}'); |
| return null; |
| } |
| }); |
| } |
| |
| static final _substituteNameVersion = RegExp(r'%([bnv])%'); |
| |
| @override |
| String get href => '$baseHref$filePath'; |
| |
| @override |
| String get location => _pathContext.toUri(packageMeta.resolvedDir).toString(); |
| |
| @override |
| String get name => _name; |
| |
| @override |
| Package get package => this; |
| |
| @override |
| PackageGraph get packageGraph => _packageGraph; |
| |
| // Workaround for mustache4dart issue where templates do not recognize |
| // inherited properties as being in-context. |
| @override |
| Iterable<Library> get publicLibraries { |
| assert(libraries.every((l) => l.packageMeta == _packageMeta)); |
| return super.publicLibraries; |
| } |
| |
| /// A map of category name to the category itself. |
| Map<String, Category> get nameToCategory { |
| if (_nameToCategory.isEmpty) { |
| Category categoryFor(String category) { |
| _nameToCategory.putIfAbsent( |
| category, () => Category(category, this, config)); |
| return _nameToCategory[category]; |
| } |
| |
| _nameToCategory[null] = Category(null, this, config); |
| for (var c in libraries.expand( |
| (l) => l.allCanonicalModelElements.whereType<Categorization>())) { |
| if (c.hasCategoryNames) { |
| for (var category in c.categoryNames) { |
| categoryFor(category).addItem(c); |
| } |
| } else { |
| // Add to the default category. |
| categoryFor(null).addItem(c); |
| } |
| } |
| } |
| return _nameToCategory; |
| } |
| |
| List<Category> _categories; |
| |
| List<Category> get categories { |
| _categories ??= nameToCategory.values.where((c) => c.name != null).toList() |
| ..sort(); |
| return _categories; |
| } |
| |
| Iterable<Category> get categoriesWithPublicLibraries => |
| categories.where((c) => c.publicLibraries.isNotEmpty); |
| |
| Iterable<Category> get documentedCategories => |
| categories.where((c) => c.isDocumented); |
| |
| Iterable<Category> get documentedCategoriesSorted { |
| // Category display order is configurable; leave the category order |
| // as defined if the order is specified. |
| if (config.categoryOrder.isEmpty) { |
| return documentedCategories; |
| } |
| return documentedCategories.toList()..sort(byName); |
| } |
| |
| bool get hasDocumentedCategories => documentedCategories.isNotEmpty; |
| |
| DartdocOptionContext _config; |
| |
| @override |
| DartdocOptionContext get config { |
| _config ??= DartdocOptionContext.fromContext( |
| packageGraph.config, |
| packageGraph.resourceProvider.getFolder(packagePath), |
| packageGraph.resourceProvider); |
| return _config; |
| } |
| |
| /// Is this the package at the top of the list? We display the first |
| /// package specially (with "Libraries" rather than the package name). |
| bool get isFirstPackage => |
| packageGraph.localPackages.isNotEmpty && |
| identical(packageGraph.localPackages.first, this); |
| |
| @override |
| bool get isSdk => packageMeta.isSdk; |
| |
| String _packagePath; |
| |
| String get packagePath { |
| _packagePath ??= _pathContext.canonicalize(packageMeta.dir.path); |
| return _packagePath; |
| } |
| |
| String get version => packageMeta.version ?? '0.0.0-unknown'; |
| |
| final PackageMeta _packageMeta; |
| |
| PackageMeta get packageMeta => _packageMeta; |
| |
| @override |
| Element get element => null; |
| |
| @override |
| List<String> get containerOrder => config.packageOrder; |
| |
| Map<String, CommentReferable> _referenceChildren; |
| @override |
| Map<String, CommentReferable> get referenceChildren { |
| if (_referenceChildren == null) { |
| _referenceChildren = {}; |
| _referenceChildren.addEntries(publicLibrariesSorted.generateEntries()); |
| // Do not override any preexisting data, and insert based on the |
| // public library sort order. |
| // TODO(jcollins-g): warn when results require package-global |
| // lookups like this. |
| _referenceChildren.addEntriesIfAbsent( |
| publicLibrariesSorted.expand((l) => l.referenceChildren.entries)); |
| } |
| return _referenceChildren; |
| } |
| |
| @override |
| Iterable<CommentReferable> get referenceParents => [packageGraph]; |
| |
| path.Context get _pathContext => _packageGraph.resourceProvider.pathContext; |
| |
| @override |
| // Packages are not interpreted by the analyzer in such a way to generate |
| // [CommentReference] nodes, so this is always empty. |
| Map<String, ModelCommentReference> get commentRefs => {}; |
| |
| @override |
| String get referenceName => 'package:$name'; |
| } |