Skip to content

Add manually-specified source code links to code #1913

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 31, 2019
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,19 @@ Unrecognized options will be ignored. Supported options:
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`.

Expand Down Expand Up @@ -391,6 +404,35 @@ other elements on the page, since those may change in future versions of Dartdoc
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
Expand Down
4 changes: 4 additions & 0 deletions dartdoc_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dartdoc:
linkToSource:
root: '.'
uriTemplate: 'https://github.com/dart-lang/dartdoc/blob/v0.28.0/%f%#L%l%'
23 changes: 23 additions & 0 deletions lib/resources/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,29 @@ h1 .category {
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;
}
Expand Down
9 changes: 7 additions & 2 deletions lib/src/dartdoc_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import 'package:args/args.dart';
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';
Expand Down Expand Up @@ -1272,7 +1273,10 @@ abstract class DartdocOptionContextBase {
/// 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
Expand Down Expand Up @@ -1573,5 +1577,6 @@ Future<List<DartdocOption>> createDartdocOptions() async {
// each DartdocOptionContext that traverses the inheritance tree itself.
]
..addAll(await createExperimentOptions())
..addAll(await createPackageWarningOptions());
..addAll(await createPackageWarningOptions())
..addAll(await createSourceLinkerOptions());
}
1 change: 1 addition & 0 deletions lib/src/html/templates.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const _partials = const <String>[
'sidebar_for_category',
'sidebar_for_enum',
'source_code',
'source_link',
'sidebar_for_library',
'accessor_getter',
'accessor_setter',
Expand Down
8 changes: 8 additions & 0 deletions lib/src/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import 'package:dartdoc/src/logging.dart';
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';
Expand Down Expand Up @@ -3215,6 +3216,13 @@ abstract class ModelElement extends Canonicalization
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.
Expand Down
119 changes: 119 additions & 0 deletions lib/src/source_linker.dart
Original file line number Diff line number Diff line change
@@ -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;
}
});
}
}
3 changes: 1 addition & 2 deletions lib/src/warnings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,10 @@ abstract class Warnable implements Canonicalization {

/// 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;
Expand Down
1 change: 1 addition & 0 deletions lib/templates/_head.html
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
3 changes: 3 additions & 0 deletions lib/templates/_source_link.html
Original file line number Diff line number Diff line change
@@ -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}}
2 changes: 1 addition & 1 deletion lib/templates/class.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ <h5>{{parent.name}} {{parent.kind}}</h5>

<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}}
Expand Down
4 changes: 3 additions & 1 deletion lib/templates/constant.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ <h5>{{parent.name}} {{parent.kind}}</h5>
</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}}
Expand Down
4 changes: 3 additions & 1 deletion lib/templates/constructor.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ <h5>{{parent.name}} {{parent.kind}}</h5>
</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">
Expand Down
2 changes: 1 addition & 1 deletion lib/templates/enum.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ <h5>{{parent.name}} {{parent.kind}}</h5>

<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}}
Expand Down
2 changes: 1 addition & 1 deletion lib/templates/function.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ <h5>{{parent.name}} {{parent.kind}}</h5>

<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}}
Expand Down
2 changes: 1 addition & 1 deletion lib/templates/library.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ <h5><span class="package-name">{{parent.name}}</span> <span class="package-kind"

<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}}
Expand Down
4 changes: 3 additions & 1 deletion lib/templates/method.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ <h5>{{parent.name}} {{parent.kind}}</h5>
</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">
Expand Down
2 changes: 1 addition & 1 deletion lib/templates/mixin.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ <h5>{{parent.name}} {{parent.kind}}</h5>

<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}}
Expand Down
4 changes: 3 additions & 1 deletion lib/templates/property.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ <h5>{{parent.name}} {{parent.kind}}</h5>
</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}}
Expand Down
2 changes: 1 addition & 1 deletion lib/templates/top_level_constant.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ <h5>{{parent.name}} {{parent.kind}}</h5>

<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}}
Expand Down
Loading