| // 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. |
| |
| /// A library for getting external source code links for Dartdoc. |
| library dartdoc.source_linker; |
| |
| import 'package:analyzer/file_system/file_system.dart'; |
| import 'package:dartdoc/src/dartdoc_options.dart'; |
| import 'package:dartdoc/src/model/model.dart'; |
| import 'package:path/path.dart' as path; |
| |
| final _uriTemplateRegExp = RegExp(r'(%[frl]%)'); |
| |
| mixin SourceLinkerOptionContext implements DartdocOptionContextBase { |
| List<String> get linkToSourceExcludes => |
| optionSet['linkToSource']['excludes'].valueAt(context); |
| |
| String? get linkToSourceRevision => |
| optionSet['linkToSource']['revision'].valueAt(context); |
| |
| String? get linkToSourceRoot => |
| optionSet['linkToSource']['root'].valueAt(context); |
| |
| String? get linkToSourceUriTemplate => |
| optionSet['linkToSource']['uriTemplate'].valueAt(context); |
| } |
| |
| List<DartdocOption<Object?>> createSourceLinkerOptions( |
| ResourceProvider resourceProvider) { |
| return [ |
| DartdocOptionSet('linkToSource', resourceProvider) |
| ..addAll([ |
| DartdocOptionArgFile<List<String>>('excludes', [], resourceProvider, |
| optionIs: OptionKind.dir, |
| help: 'A list of directories to exclude from linking to a source ' |
| 'code repository.'), |
| // TODO(jcollins-g): Use [DartdocOptionArgSynth], possibly in |
| // combination with a repository type and the root directory, and get |
| // revision number automatically. |
| DartdocOptionArgOnly<String?>('revision', null, resourceProvider, |
| help: 'Revision number to insert into the URI.'), |
| DartdocOptionArgFile<String?>('root', null, resourceProvider, |
| optionIs: OptionKind.dir, |
| help: |
| 'Path to a local directory that is the root of the repository ' |
| 'we link to. All source code files under this directory will ' |
| 'be linked.'), |
| DartdocOptionArgFile<String?>('uriTemplate', null, resourceProvider, |
| help: ''' |
| Substitute into this template to generate a uri for an element's source code. |
| Dartdoc dynamically substitutes the following fields into the template: |
| %f%: Relative path of file to the repository root |
| %r%: Revision number |
| %l%: Line number'''), |
| ]) |
| ]; |
| } |
| |
| class SourceLinker { |
| final List<String> excludes; |
| final int lineNumber; |
| final String sourceFileName; |
| final String? revision; |
| final String? root; |
| final String? uriTemplate; |
| |
| /// Most users of this class should use the [SourceLinker.fromElement] factory |
| /// instead. This constructor is public for testing. |
| SourceLinker( |
| {required this.excludes, |
| required this.lineNumber, |
| required this.sourceFileName, |
| this.revision, |
| this.root, |
| this.uriTemplate}) { |
| if (revision != null || root != null || uriTemplate != null) { |
| if (root == null || uriTemplate == null) { |
| throw DartdocOptionError( |
| 'linkToSource root and uriTemplate must both be specified to ' |
| 'generate repository links'); |
| } |
| var uriTemplateValue = uriTemplate; |
| if (uriTemplateValue != null && |
| uriTemplateValue.contains('%r%') && |
| revision == null) { |
| throw DartdocOptionError( |
| r'%r% specified in uriTemplate, but no revision available'); |
| } |
| } |
| } |
| |
| /// Build a SourceLinker from a ModelElement. |
| factory SourceLinker.fromElement(ModelElement element) { |
| SourceLinkerOptionContext config = element.config; |
| return SourceLinker( |
| excludes: config.linkToSourceExcludes, |
| // TODO(jcollins-g): disallow defaulting? Some elements come back without |
| // a line number right now. |
| lineNumber: element.characterLocation?.lineNumber ?? 1, |
| sourceFileName: element.sourceFileName, |
| revision: config.linkToSourceRevision, |
| root: config.linkToSourceRoot, |
| uriTemplate: config.linkToSourceUriTemplate, |
| ); |
| } |
| |
| String href() { |
| var root = this.root; |
| var uriTemplate = this.uriTemplate; |
| if (root == null || uriTemplate == null) { |
| return ''; |
| } |
| if (!path.isWithin(root, sourceFileName) || |
| excludes |
| .any((String exclude) => path.isWithin(exclude, sourceFileName))) { |
| return ''; |
| } |
| return uriTemplate.replaceAllMapped(_uriTemplateRegExp, (match) { |
| return switch (match[1]) { |
| '%f%' => path.url |
| .joinAll(path.split(path.relative(sourceFileName, from: root))), |
| '%r%' => revision!, |
| '%l%' => lineNumber.toString(), |
| _ => '' |
| }; |
| }); |
| } |
| } |