Add manually-specified source code links to code (#1913)
* Link to source now works, minus icons
* Add material icons
* Refactor dartdoc_test to avoid asynchronous delete collisions
* dartfmt
* Fix windows
* dartfmt
* Tweak padding and change icon to description
* Review comments
* Add example and use it in the dartdoc package.
* dartfmt
diff --git a/README.md b/README.md
index 5880cb5..503487f 100644
--- a/README.md
+++ b/README.md
@@ -163,6 +163,19 @@
No branch is considered to be "stable".
* `%n%`: The name of this package, as defined in pubspec.yaml.
* `%v%`: The version of this package as defined in pubspec.yaml.
+ * **linkToSource**: Generate links to a source code repository based on given templates and
+ revision information.
+ * **excludes**: A list of directories to exclude from processing source links.
+ * **root**: The directory to consider the 'root' for inserting relative paths into the template.
+ Source code outside the root directory will not be linked.
+ * **uriTemplate**: A template to substitute revision and file path information. If revision
+ is present in the template but not specified, or if root is not specified, dartdoc will
+ throw an exception. To hard-code a revision, don't specify it with `%r%`.
+
+ The following strings will be substituted in to complete the URL:
+ * `%f%`: Relative path of file to the repository root
+ * `%r%`: Revision
+ * `%l%`: Line number
* **warnings**: Specify otherwise ignored or set-to-error warnings to simply warn. See the lists
of valid warnings in the command line help for `--errors`, `--warnings`, and `--ignore`.
@@ -391,6 +404,35 @@
If `--auto-include-dependencies` flag is provided, dartdoc tries to automatically add
all the used libraries, even from other packages, to the list of the documented libraries.
+### Using link-to-source
+
+The source linking feature in dartdoc is a little tricky to use, since pub packages do not actually
+include enough information to link back to source code and that's the context in which documentation
+is generated for the pub site. This means that for now, it must be manually specified in
+dartdoc_options.yaml what revision to use. It is currently recommended practice to
+specify a revision in dartdoc_options.yaml that points to the same revision as your public package.
+If you're using a documentation staging system outside of Dart's pub site, override the template and
+revision on the command line with the head revision number. You can use the branch name,
+but generated docs will generate locations that may start drifting with further changes to the branch.
+
+Example dartdoc_options.yaml:
+```yaml
+link-to-source:
+ root: '.'
+ uriTemplate: 'https://github.com/dart-lang/dartdoc/blob/v0.28.0/%f%#L%l%'
+```
+
+Example staging command line:
+```bash
+pub global run dartdoc --link-to-source-root '.' --link-to-source-revision 6fac6f770d271312c88e8ae881861702a9a605be --link-to-source-uri-template 'https://github.com/dart-lang/dartdoc/blob/%r%/%f#L%l%'
+```
+
+This gets more complicated with `--auto-include-dependencies` as these command line flags
+will override all settings from individual packages. In that case, to preserve
+source links from third party packages it may be necessary to generate
+dartdoc_options.yaml options for each package you are intending to add source links
+to yourself.
+
## Issues and bugs
Please file reports on the [GitHub Issue Tracker][]. Issues are labeled with
diff --git a/dartdoc_options.yaml b/dartdoc_options.yaml
new file mode 100644
index 0000000..658ddd1
--- /dev/null
+++ b/dartdoc_options.yaml
@@ -0,0 +1,4 @@
+dartdoc:
+ linkToSource:
+ root: '.'
+ uriTemplate: 'https://github.com/dart-lang/dartdoc/blob/v0.28.0/%f%#L%l%'
diff --git a/lib/resources/styles.css b/lib/resources/styles.css
index 7d8cc1d..59e0590 100644
--- a/lib/resources/styles.css
+++ b/lib/resources/styles.css
@@ -437,6 +437,29 @@
vertical-align: middle;
}
+.source-link {
+ padding: 18px 4px;
+ font-size: 12px;
+ vertical-align: middle;
+}
+
+@media (max-width: 768px) {
+ .source-link {
+ padding: 7px 2px;
+ font-size: 10px;
+ }
+}
+
+#external-links {
+ float: right;
+}
+
+.btn-group {
+ position: relative;
+ display: inline-flex;
+ vertical-align: middle;
+}
+
p.firstline {
font-weight: bold;
}
diff --git a/lib/src/dartdoc_options.dart b/lib/src/dartdoc_options.dart
index e97fe4f..13b9d88 100644
--- a/lib/src/dartdoc_options.dart
+++ b/lib/src/dartdoc_options.dart
@@ -22,6 +22,7 @@
import 'package:dartdoc/dartdoc.dart';
import 'package:dartdoc/src/experiment_options.dart';
import 'package:dartdoc/src/io_utils.dart';
+import 'package:dartdoc/src/source_linker.dart';
import 'package:dartdoc/src/tool_runner.dart';
import 'package:dartdoc/src/tuple.dart';
import 'package:dartdoc/src/warnings.dart';
@@ -1272,7 +1273,10 @@
/// a single [ModelElement], [Package], [Category] and so forth has a single context
/// and so this can be made a member variable of those structures.
class DartdocOptionContext extends DartdocOptionContextBase
- with DartdocExperimentOptionContext, PackageWarningOptionContext {
+ with
+ DartdocExperimentOptionContext,
+ PackageWarningOptionContext,
+ SourceLinkerOptionContext {
@override
final DartdocOptionSet optionSet;
@override
@@ -1573,5 +1577,6 @@
// each DartdocOptionContext that traverses the inheritance tree itself.
]
..addAll(await createExperimentOptions())
- ..addAll(await createPackageWarningOptions());
+ ..addAll(await createPackageWarningOptions())
+ ..addAll(await createSourceLinkerOptions());
}
diff --git a/lib/src/html/templates.dart b/lib/src/html/templates.dart
index 178c40e..ee30023 100644
--- a/lib/src/html/templates.dart
+++ b/lib/src/html/templates.dart
@@ -30,6 +30,7 @@
'sidebar_for_category',
'sidebar_for_enum',
'source_code',
+ 'source_link',
'sidebar_for_library',
'accessor_getter',
'accessor_setter',
diff --git a/lib/src/model.dart b/lib/src/model.dart
index 9bfe5fe..0e5cc90 100644
--- a/lib/src/model.dart
+++ b/lib/src/model.dart
@@ -56,6 +56,7 @@
import 'package:dartdoc/src/markdown_processor.dart' show Documentation;
import 'package:dartdoc/src/model_utils.dart';
import 'package:dartdoc/src/package_meta.dart' show PackageMeta, FileContents;
+import 'package:dartdoc/src/source_linker.dart';
import 'package:dartdoc/src/special_elements.dart';
import 'package:dartdoc/src/tuple.dart';
import 'package:dartdoc/src/utils.dart';
@@ -3215,6 +3216,13 @@
return _documentationFrom;
}
+ bool get hasSourceHref => sourceHref.isNotEmpty;
+ String _sourceHref;
+ String get sourceHref {
+ _sourceHref ??= new SourceLinker.fromElement(this).href();
+ return _sourceHref;
+ }
+
/// Returns the ModelElement(s) from which we will get documentation.
/// Can be more than one if this is a Field composing documentation from
/// multiple Accessors.
diff --git a/lib/src/source_linker.dart b/lib/src/source_linker.dart
new file mode 100644
index 0000000..220b518
--- /dev/null
+++ b/lib/src/source_linker.dart
@@ -0,0 +1,119 @@
+// 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:dartdoc/src/dartdoc_options.dart';
+import 'package:dartdoc/src/model.dart';
+import 'package:path/path.dart' as pathLib;
+
+final uriTemplateRegexp = new RegExp(r'(%[frl]%)');
+
+abstract class 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);
+}
+
+Future<List<DartdocOption>> createSourceLinkerOptions() async {
+ return <DartdocOption>[
+ new DartdocOptionSet('linkToSource')
+ ..addAll([
+ new DartdocOptionArgFile<List<String>>('excludes', [],
+ isDir: true,
+ 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
+ new DartdocOptionArgOnly<String>('revision', null,
+ help: 'Revision number to insert into the URI.'),
+ new DartdocOptionArgFile<String>('root', null,
+ isDir: true,
+ 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.'),
+ new DartdocOptionArgFile<String>('uriTemplate', null,
+ 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(
+ {List<String> this.excludes,
+ int this.lineNumber,
+ String this.sourceFileName,
+ String this.revision,
+ String this.root,
+ String this.uriTemplate}) {
+ assert(excludes != null, 'linkToSource excludes can not be null');
+ 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');
+ }
+ if (uriTemplate.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 new SourceLinker(
+ excludes: config.linkToSourceExcludes,
+ // TODO(jcollins-g): disallow defaulting? Some elements come back without
+ // a line number right now.
+ lineNumber: element.lineAndColumn?.item1 ?? 1,
+ sourceFileName: element.sourceFileName,
+ revision: config.linkToSourceRevision,
+ root: config.linkToSourceRoot,
+ uriTemplate: config.linkToSourceUriTemplate,
+ );
+ }
+
+ String href() {
+ if (sourceFileName == null || root == null || uriTemplate == null)
+ return '';
+ if (!pathLib.isWithin(root, sourceFileName) ||
+ excludes
+ .any((String exclude) => pathLib.isWithin(exclude, sourceFileName)))
+ return '';
+ return uriTemplate.replaceAllMapped(uriTemplateRegexp, (match) {
+ switch (match[1]) {
+ case '%f%':
+ var urlContext = new pathLib.Context(style: pathLib.Style.url);
+ return urlContext.joinAll(
+ pathLib.split(pathLib.relative(sourceFileName, from: root)));
+ break;
+ case '%r%':
+ return revision;
+ break;
+ case '%l%':
+ return lineNumber.toString();
+ break;
+ }
+ });
+ }
+}
diff --git a/lib/src/warnings.dart b/lib/src/warnings.dart
index 93c5e61..1ac42e8 100644
--- a/lib/src/warnings.dart
+++ b/lib/src/warnings.dart
@@ -208,11 +208,10 @@
/// Something that can be located for warning purposes.
abstract class Locatable {
+ List<Locatable> get documentationFrom;
String get fullyQualifiedName;
String get href;
- List<Locatable> get documentationFrom;
-
/// A string indicating the URI of this Locatable, usually derived from
/// [Element.location].
String get location;
diff --git a/lib/templates/_head.html b/lib/templates/_head.html
index e369aba..3628bd9 100644
--- a/lib/templates/_head.html
+++ b/lib/templates/_head.html
@@ -18,6 +18,7 @@
{{/htmlBase}}
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500,400i,400,300|Source+Sans+Pro:400,300,700" rel="stylesheet">
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="static-assets/github.css">
<link rel="stylesheet" href="static-assets/styles.css">
<link rel="icon" href="static-assets/favicon.png">
diff --git a/lib/templates/_source_link.html b/lib/templates/_source_link.html
new file mode 100644
index 0000000..7db4cf9
--- /dev/null
+++ b/lib/templates/_source_link.html
@@ -0,0 +1,3 @@
+{{#hasSourceHref}}
+ <div id="external-links" class="btn-group"><a title="View source code" class="source-link" href="{{{sourceHref}}}"><i class="material-icons">description</i></a></div>
+{{/hasSourceHref}}
\ No newline at end of file
diff --git a/lib/templates/class.html b/lib/templates/class.html
index 9a6505e..d511c4a 100644
--- a/lib/templates/class.html
+++ b/lib/templates/class.html
@@ -8,7 +8,7 @@
<div class="col-xs-12 col-sm-9 col-md-8 main-content">
{{#self}}
- <h1>{{{nameWithGenerics}}} {{kind}} {{>categorization}}</h1>
+ <div>{{>source_link}}<h1>{{{nameWithGenerics}}} {{kind}} {{>categorization}}</h1></div>
{{/self}}
{{#clazz}}
diff --git a/lib/templates/constant.html b/lib/templates/constant.html
index 0d7c09d..f8ae237 100644
--- a/lib/templates/constant.html
+++ b/lib/templates/constant.html
@@ -7,7 +7,9 @@
</div><!--/.sidebar-offcanvas-left-->
<div class="col-xs-12 col-sm-9 col-md-8 main-content">
- <h1>{{self.name}} {{self.kind}}</h1>
+ {{#self}}
+ <div>{{>source_link}}<h1>{{{name}}} {{kind}}</h1></div>
+ {{/self}}
<section class="multi-line-signature">
{{#property}}
diff --git a/lib/templates/constructor.html b/lib/templates/constructor.html
index 9422f14..a9f488f 100644
--- a/lib/templates/constructor.html
+++ b/lib/templates/constructor.html
@@ -7,7 +7,9 @@
</div><!--/.sidebar-offcanvas-left-->
<div class="col-xs-12 col-sm-9 col-md-8 main-content">
- <h1>{{{self.nameWithGenerics}}} {{self.kind}}</h1>
+ {{#self}}
+ <div>{{>source_link}}<h1>{{{nameWithGenerics}}} {{kind}}</h1></div>
+ {{/self}}
{{#constructor}}
<section class="multi-line-signature">
diff --git a/lib/templates/enum.html b/lib/templates/enum.html
index 6a72ee7..25a0ac1 100644
--- a/lib/templates/enum.html
+++ b/lib/templates/enum.html
@@ -8,7 +8,7 @@
<div class="col-xs-12 col-sm-9 col-md-8 main-content">
{{#self}}
- <h1>{{{name}}} {{kind}} {{>categorization}}</h1>
+ <div>{{>source_link}}<h1>{{{name}}} {{kind}} {{>categorization}}</h1></div>
{{/self}}
{{#eNum}}
diff --git a/lib/templates/function.html b/lib/templates/function.html
index 73a79ad..c13f320 100644
--- a/lib/templates/function.html
+++ b/lib/templates/function.html
@@ -8,7 +8,7 @@
<div class="col-xs-12 col-sm-9 col-md-8 main-content">
{{#self}}
- <h1>{{{nameWithGenerics}}} {{kind}} {{>categorization}}</h1>
+ <div>{{>source_link}}<h1>{{{nameWithGenerics}}} {{kind}} {{>categorization}}</h1></div>
{{/self}}
{{#function}}
diff --git a/lib/templates/library.html b/lib/templates/library.html
index 1c3b56c..ab1c240 100644
--- a/lib/templates/library.html
+++ b/lib/templates/library.html
@@ -8,7 +8,7 @@
<div class="col-xs-12 col-sm-9 col-md-8 main-content">
{{#self}}
- <h1>{{{name}}} {{kind}} {{>categorization}}</h1>
+ <div>{{>source_link}}<h1>{{{name}}} {{kind}} {{>categorization}}</h1></div>
{{/self}}
{{#library}}
diff --git a/lib/templates/method.html b/lib/templates/method.html
index 4c4f021..ff612ad 100644
--- a/lib/templates/method.html
+++ b/lib/templates/method.html
@@ -7,7 +7,9 @@
</div><!--/.sidebar-offcanvas-->
<div class="col-xs-12 col-sm-9 col-md-8 main-content">
- <h1>{{{self.nameWithGenerics}}} {{self.kind}}</h1>
+ {{#self}}
+ <div>{{>source_link}}<h1>{{{nameWithGenerics}}} {{kind}}</h1></div>
+ {{/self}}
{{#method}}
<section class="multi-line-signature">
diff --git a/lib/templates/mixin.html b/lib/templates/mixin.html
index 1902b4e..20ae6a3 100644
--- a/lib/templates/mixin.html
+++ b/lib/templates/mixin.html
@@ -8,7 +8,7 @@
<div class="col-xs-12 col-sm-9 col-md-8 main-content">
{{#self}}
- <h1>{{{nameWithGenerics}}} {{kind}} {{>categorization}}</h1>
+ <div>{{>source_link}}<h1>{{{nameWithGenerics}}} {{kind}} {{>categorization}}</h1></div>
{{/self}}
{{#mixin}}
diff --git a/lib/templates/property.html b/lib/templates/property.html
index e6b3ded..915c420 100644
--- a/lib/templates/property.html
+++ b/lib/templates/property.html
@@ -7,7 +7,9 @@
</div><!--/.sidebar-offcanvas-->
<div class="col-xs-12 col-sm-9 col-md-8 main-content">
- <h1>{{self.name}} {{self.kind}}</h1>
+ {{#self}}
+ <div>{{>source_link}}<h1>{{name}} {{kind}}</h1></div>
+ {{/self}}
{{#self}}
{{#hasNoGetterSetter}}
diff --git a/lib/templates/top_level_constant.html b/lib/templates/top_level_constant.html
index 47c8340..60499ef 100644
--- a/lib/templates/top_level_constant.html
+++ b/lib/templates/top_level_constant.html
@@ -8,7 +8,7 @@
<div class="col-xs-12 col-sm-9 col-md-8 main-content">
{{#self}}
- <h1>{{{name}}} {{kind}} {{>categorization}}</h1>
+ <div>{{>source_link}}<h1>{{{name}}} {{kind}} {{>categorization}}</h1></div>
<section class="multi-line-signature">
{{>name_summary}}
diff --git a/lib/templates/top_level_property.html b/lib/templates/top_level_property.html
index 4e6a22e..f3682c2 100644
--- a/lib/templates/top_level_property.html
+++ b/lib/templates/top_level_property.html
@@ -8,7 +8,7 @@
<div class="col-xs-12 col-sm-9 col-md-8 main-content">
{{#self}}
- <h1>{{{name}}} {{kind}} {{>categorization}}</h1>
+ <div>{{>source_link}}<h1>{{{name}}} {{kind}} {{>categorization}}</h1></div>
{{#hasNoGetterSetter}}
<section class="multi-line-signature">
diff --git a/lib/templates/typedef.html b/lib/templates/typedef.html
index c0b072a..eb064ca 100644
--- a/lib/templates/typedef.html
+++ b/lib/templates/typedef.html
@@ -8,7 +8,7 @@
<div class="col-xs-12 col-sm-9 col-md-8 main-content">
{{#self}}
- <h1>{{{nameWithGenerics}}} {{kind}} {{>categorization}}</h1>
+ <div>{{>source_link}}<h1>{{{nameWithGenerics}}} {{kind}} {{>categorization}}</h1></div>
{{/self}}
<section class="multi-line-signature">
diff --git a/test/dartdoc_test.dart b/test/dartdoc_test.dart
index 82c27b9..664d7df 100644
--- a/test/dartdoc_test.dart
+++ b/test/dartdoc_test.dart
@@ -26,11 +26,9 @@
void main() {
group('dartdoc with generators', () {
Directory tempDir;
- List<String> outputParam;
setUpAll(() async {
tempDir = Directory.systemTemp.createTempSync('dartdoc.test.');
- outputParam = ['--output', tempDir.path];
DartdocOptionSet optionSet = await DartdocOptionSet.fromOptionGenerators(
'dartdoc', [createLoggingOptions]);
optionSet.parseArguments([]);
@@ -38,29 +36,38 @@
new DartdocLoggingOptionContext(optionSet, Directory.current));
});
+ setUp(() async {
+ tempDir = Directory.systemTemp.createTempSync('dartdoc.test.');
+ });
+
tearDown(() async {
- tempDir.listSync().forEach((FileSystemEntity f) {
- f.deleteSync(recursive: true);
- });
+ tempDir.deleteSync(recursive: true);
});
Future<Dartdoc> buildDartdoc(
- List<String> argv, Directory packageRoot) async {
+ List<String> argv, Directory packageRoot, Directory tempDir) async {
return await Dartdoc.withDefaultGenerators(await generatorContextFromArgv(
- argv..addAll(['--input', packageRoot.path])..addAll(outputParam)));
+ argv
+ ..addAll(['--input', packageRoot.path, '--output', tempDir.path])));
}
group('Option handling', () {
Dartdoc dartdoc;
DartdocResults results;
PackageGraph p;
+ Directory tempDir;
setUpAll(() async {
- dartdoc = await buildDartdoc([], testPackageOptions);
+ tempDir = Directory.systemTemp.createTempSync('dartdoc.test.');
+ dartdoc = await buildDartdoc([], testPackageOptions, tempDir);
results = await dartdoc.generateDocsBase();
p = results.packageGraph;
});
+ tearDownAll(() async {
+ tempDir.deleteSync(recursive: true);
+ });
+
test('generator parameters', () async {
File favicon = new File(
pathLib.joinAll([tempDir.path, 'static-assets', 'favicon.png']));
@@ -93,7 +100,7 @@
});
test('errors generate errors even when warnings are off', () async {
- Dartdoc dartdoc = await buildDartdoc([], testPackageToolError);
+ Dartdoc dartdoc = await buildDartdoc([], testPackageToolError, tempDir);
DartdocResults results = await dartdoc.generateDocsBase();
PackageGraph p = results.packageGraph;
Iterable<String> unresolvedToolErrors = p
@@ -112,15 +119,21 @@
group('Option handling with cross-linking', () {
DartdocResults results;
Package testPackageOptions;
+ Directory tempDir;
setUpAll(() async {
+ tempDir = Directory.systemTemp.createTempSync('dartdoc.test.');
results = await (await buildDartdoc(
- ['--link-to-remote'], testPackageOptionsImporter))
+ ['--link-to-remote'], testPackageOptionsImporter, tempDir))
.generateDocsBase();
testPackageOptions = results.packageGraph.packages
.firstWhere((Package p) => p.name == 'test_package_options');
});
+ tearDownAll(() async {
+ tempDir.deleteSync(recursive: true);
+ });
+
test('linkToUrl', () async {
Library main = testPackageOptions.allLibraries
.firstWhere((Library l) => l.name == 'main');
@@ -145,7 +158,8 @@
});
test('with broken reexport chain', () async {
- Dartdoc dartdoc = await buildDartdoc([], testPackageImportExportError);
+ Dartdoc dartdoc =
+ await buildDartdoc([], testPackageImportExportError, tempDir);
DartdocResults results = await dartdoc.generateDocsBase();
PackageGraph p = results.packageGraph;
Iterable<String> unresolvedExportWarnings = p
@@ -162,7 +176,8 @@
group('include/exclude parameters', () {
test('with config file', () async {
- Dartdoc dartdoc = await buildDartdoc([], testPackageIncludeExclude);
+ Dartdoc dartdoc =
+ await buildDartdoc([], testPackageIncludeExclude, tempDir);
DartdocResults results = await dartdoc.generateDocs();
PackageGraph p = results.packageGraph;
expect(p.localPublicLibraries.map((l) => l.name),
@@ -170,8 +185,8 @@
});
test('with include command line argument', () async {
- Dartdoc dartdoc = await buildDartdoc(
- ['--include', 'another_included'], testPackageIncludeExclude);
+ Dartdoc dartdoc = await buildDartdoc(['--include', 'another_included'],
+ testPackageIncludeExclude, tempDir);
DartdocResults results = await dartdoc.generateDocs();
PackageGraph p = results.packageGraph;
expect(p.localPublicLibraries.length, equals(1));
@@ -180,7 +195,7 @@
test('with exclude command line argument', () async {
Dartdoc dartdoc = await buildDartdoc(
- ['--exclude', 'more_included'], testPackageIncludeExclude);
+ ['--exclude', 'more_included'], testPackageIncludeExclude, tempDir);
DartdocResults results = await dartdoc.generateDocs();
PackageGraph p = results.packageGraph;
expect(p.localPublicLibraries.length, equals(1));
@@ -190,7 +205,7 @@
});
test('package without version produces valid semver in docs', () async {
- Dartdoc dartdoc = await buildDartdoc([], testPackageMinimumDir);
+ Dartdoc dartdoc = await buildDartdoc([], testPackageMinimumDir, tempDir);
DartdocResults results = await dartdoc.generateDocs();
PackageGraph p = results.packageGraph;
expect(p.defaultPackage.version, equals('0.0.0-unknown'));
@@ -198,7 +213,7 @@
test('basic interlinking test', () async {
Dartdoc dartdoc =
- await buildDartdoc(['--link-to-remote'], testPackageDir);
+ await buildDartdoc(['--link-to-remote'], testPackageDir, tempDir);
DartdocResults results = await dartdoc.generateDocs();
PackageGraph p = results.packageGraph;
Package meta = p.publicPackages.firstWhere((p) => p.name == 'meta');
@@ -220,25 +235,49 @@
expect(useSomethingInTheSdk.modelType.linkedName, contains(stringLink));
});
- test('generate docs for ${pathLib.basename(testPackageDir.path)} works',
- () async {
- Dartdoc dartdoc = await buildDartdoc([], testPackageDir);
+ group('validate basic doc generation', () {
+ Dartdoc dartdoc;
+ DartdocResults results;
+ Directory tempDir;
- DartdocResults results = await dartdoc.generateDocs();
- expect(results.packageGraph, isNotNull);
+ setUpAll(() async {
+ tempDir = Directory.systemTemp.createTempSync('dartdoc.test.');
+ dartdoc = await buildDartdoc([], testPackageDir, tempDir);
+ results = await dartdoc.generateDocs();
+ });
- PackageGraph packageGraph = results.packageGraph;
- Package p = packageGraph.defaultPackage;
- expect(p.name, 'test_package');
- expect(p.hasDocumentationFile, isTrue);
- // Total number of public libraries in test_package.
- expect(packageGraph.defaultPackage.publicLibraries, hasLength(12));
- expect(packageGraph.localPackages.length, equals(1));
+ tearDownAll(() async {
+ tempDir.deleteSync(recursive: true);
+ });
+
+ test('generate docs for ${pathLib.basename(testPackageDir.path)} works',
+ () async {
+ expect(results.packageGraph, isNotNull);
+ PackageGraph packageGraph = results.packageGraph;
+ Package p = packageGraph.defaultPackage;
+ expect(p.name, 'test_package');
+ expect(p.hasDocumentationFile, isTrue);
+ // Total number of public libraries in test_package.
+ expect(packageGraph.defaultPackage.publicLibraries, hasLength(12));
+ expect(packageGraph.localPackages.length, equals(1));
+ });
+
+ test('source code links are visible', () async {
+ // Picked this object as this library explicitly should never contain
+ // a library directive, so we can predict what line number it will be.
+ File anonymousOutput = new File(pathLib.join(tempDir.path,
+ 'anonymous_library', 'anonymous_library-library.html'));
+ expect(anonymousOutput.existsSync(), isTrue);
+ expect(
+ anonymousOutput.readAsStringSync(),
+ contains(
+ r'<a title="View source code" class="source-link" href="https://github.com/dart-lang/dartdoc/blob/master/testing/test_package/lib/anonymous_library.dart#L1"><i class="material-icons">description</i></a>'));
+ });
});
test('generate docs for ${pathLib.basename(testPackageBadDir.path)} fails',
() async {
- Dartdoc dartdoc = await buildDartdoc([], testPackageBadDir);
+ Dartdoc dartdoc = await buildDartdoc([], testPackageBadDir, tempDir);
try {
await dartdoc.generateDocs();
@@ -251,7 +290,8 @@
'from analysis_options');
test('generate docs for a package that does not have a readme', () async {
- Dartdoc dartdoc = await buildDartdoc([], testPackageWithNoReadme);
+ Dartdoc dartdoc =
+ await buildDartdoc([], testPackageWithNoReadme, tempDir);
DartdocResults results = await dartdoc.generateDocs();
expect(results.packageGraph, isNotNull);
@@ -265,7 +305,7 @@
test('generate docs including a single library', () async {
Dartdoc dartdoc =
- await buildDartdoc(['--include', 'fake'], testPackageDir);
+ await buildDartdoc(['--include', 'fake'], testPackageDir, tempDir);
DartdocResults results = await dartdoc.generateDocs();
expect(results.packageGraph, isNotNull);
@@ -279,7 +319,7 @@
test('generate docs excluding a single library', () async {
Dartdoc dartdoc =
- await buildDartdoc(['--exclude', 'fake'], testPackageDir);
+ await buildDartdoc(['--exclude', 'fake'], testPackageDir, tempDir);
DartdocResults results = await dartdoc.generateDocs();
expect(results.packageGraph, isNotNull);
@@ -293,7 +333,8 @@
});
test('generate docs for package with embedder yaml', () async {
- Dartdoc dartdoc = await buildDartdoc([], testPackageWithEmbedderYaml);
+ Dartdoc dartdoc =
+ await buildDartdoc([], testPackageWithEmbedderYaml, tempDir);
DartdocResults results = await dartdoc.generateDocs();
expect(results.packageGraph, isNotNull);
diff --git a/test/source_linker_test.dart b/test/source_linker_test.dart
new file mode 100644
index 0000000..e31014a
--- /dev/null
+++ b/test/source_linker_test.dart
@@ -0,0 +1,85 @@
+// 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.
+
+library dartdoc.source_linker_test;
+
+import 'package:dartdoc/src/dartdoc_options.dart';
+import 'package:dartdoc/src/source_linker.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('Source link computations', () {
+ test('Basic usage', () {
+ var sourceLinkerHref = () => new SourceLinker(
+ excludes: [],
+ lineNumber: 14,
+ root: 'path',
+ sourceFileName: 'path/to/file.dart',
+ revision: '01234abcd',
+ uriTemplate: 'http://github.com/base/%r%/%f%/L%l%')
+ .href();
+ expect(sourceLinkerHref(),
+ equals('http://github.com/base/01234abcd/to/file.dart/L14'));
+ });
+
+ test('Throw when missing a revision if one is in the template', () {
+ var sourceLinkerHref = () => new SourceLinker(
+ excludes: [],
+ lineNumber: 20,
+ root: 'path',
+ sourceFileName: 'path/to/file.dart',
+ uriTemplate: 'http://github.com/base/%r%/%f%/L%l%')
+ .href();
+ expect(sourceLinkerHref, throwsA(TypeMatcher<DartdocOptionError>()));
+ });
+
+ test('Allow a missing revision as long as it is not in the template', () {
+ var sourceLinkerHref = () => new SourceLinker(
+ excludes: [],
+ lineNumber: 71,
+ root: 'path',
+ sourceFileName: 'path/to/file.dart',
+ uriTemplate: 'http://github.com/base/master/%f%/L%l%')
+ .href();
+ expect(sourceLinkerHref(),
+ equals('http://github.com/base/master/to/file.dart/L71'));
+ });
+
+ test('Throw if only revision specified', () {
+ var sourceLinkerHref = () => new SourceLinker(
+ excludes: [],
+ lineNumber: 20,
+ revision: '12345',
+ ).href();
+ expect(sourceLinkerHref, throwsA(TypeMatcher<DartdocOptionError>()));
+ });
+
+ test('Hide a path inside an exclude', () {
+ var sourceLinkerHref = () => new SourceLinker(
+ excludes: ['path/under/exclusion'],
+ lineNumber: 14,
+ root: 'path',
+ sourceFileName: 'path/under/exclusion/file.dart',
+ revision: '01234abcd',
+ uriTemplate: 'http://github.com/base/%r%/%f%/L%l%')
+ .href();
+ expect(sourceLinkerHref(), equals(''));
+ });
+
+ test('Check that paths outside exclusions work', () {
+ var sourceLinkerHref = () => new SourceLinker(
+ excludes: ['path/under/exclusion'],
+ lineNumber: 14,
+ root: 'path',
+ sourceFileName: 'path/not/under/exclusion/file.dart',
+ revision: '01234abcd',
+ uriTemplate: 'http://github.com/base/%r%/%f%/L%l%')
+ .href();
+ expect(
+ sourceLinkerHref(),
+ equals(
+ 'http://github.com/base/01234abcd/not/under/exclusion/file.dart/L14'));
+ });
+ });
+}
diff --git a/testing/test_package/dartdoc_options.yaml b/testing/test_package/dartdoc_options.yaml
index fb0a3df..305e927 100644
--- a/testing/test_package/dartdoc_options.yaml
+++ b/testing/test_package/dartdoc_options.yaml
@@ -16,3 +16,6 @@
linux: ['/bin/sh', '-c', 'echo']
windows: ['C:\\Windows\\System32\\cmd.exe', '/c', 'echo']
description: 'Works on everything'
+ linkToSource:
+ root: '.'
+ uriTemplate: 'https://github.com/dart-lang/dartdoc/blob/master/testing/test_package/%f%#L%l%'
diff --git a/tool/grind.dart b/tool/grind.dart
index d707afc..a5cfdd4 100644
--- a/tool/grind.dart
+++ b/tool/grind.dart
@@ -779,10 +779,20 @@
var launcher = new SubprocessLauncher('build');
await launcher.runStreamed(sdkBin('pub'),
['run', 'build_runner', 'build', '--delete-conflicting-outputs']);
+
+ // TODO(jcollins-g): port to build system?
+ String version = _getPackageVersion();
+ File dartdoc_options = new File('dartdoc_options.yaml');
+ await dartdoc_options.writeAsString('''dartdoc:
+ linkToSource:
+ root: '.'
+ uriTemplate: 'https://github.com/dart-lang/dartdoc/blob/v${version}/%f%#L%l%'
+''');
}
/// Paths in this list are relative to lib/.
final _generated_files_list = <String>[
+ '../dartdoc_options.yaml',
'src/html/resources.g.dart',
'src/version.dart',
].map((s) => pathLib.joinAll(pathLib.posix.split(s)));
@@ -803,8 +813,7 @@
}
}
- await launcher.runStreamed(sdkBin('pub'),
- ['run', 'build_runner', 'build', '--delete-conflicting-outputs']);
+ await build();
for (String relPath in _generated_files_list) {
File newVersion = new File(pathLib.join('lib', relPath));
if (!await newVersion.exists()) {