Skip to content

Commit 1c911bc

Browse files
chalinkeertip
authored andcommitted
feat: support @example insertion of .md file fragments (#1295)
* Fix link in CONTRIBUTING.md * feat: support @example insertion of .md file fragments Fixes #1105
1 parent 4bfe72b commit 1c911bc

File tree

9 files changed

+98
-32
lines changed

9 files changed

+98
-32
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ yet in the issue tracker, start by opening an issue. Thanks!
2323

2424
1. `grind` is needed to run dartdoc integration tests, see installed via `pub global activate grinder`.
2525
2. When a change is user-facing, please add a new entry to the [changelog](https://github.com/dart-lang/dartdoc/blob/master/CHANGELOG.md)
26-
3. Please include a test for your change. `dartdoc` has both `package:test`-style unittests as well as integration tests. To run the unittests, use `dart test/all.dart`. Most changes can be tested via a unittest, but some require modifying the (test_package)[https://github.com/dart-lang/dartdoc/tree/master/testing/test_package] and regenerating its docs via `grind update-test-package-docs`.
26+
3. Please include a test for your change. `dartdoc` has both `package:test`-style unittests as well as integration tests. To run the unittests, use `dart test/all.dart`. Most changes can be tested via a unittest, but some require modifying the [test_package](https://github.com/dart-lang/dartdoc/tree/master/testing/test_package) and regenerating its docs via `grind update-test-package-docs`.
2727
4. Be sure to format your Dart code using `dartfmt -w`, otherwise travis will complain.
2828
5. Post your change via a pull request for review and integration!
2929

lib/src/model.dart

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1716,32 +1716,82 @@ abstract class ModelElement implements Comparable, Nameable, Documentable {
17161716
return lib;
17171717
}
17181718

1719-
// process the {@example ...} in comments and inject the example
1720-
// code into the doc commment.
1721-
// {@example core/ts/bootstrap/bootstrap.ts region='bootstrap'}
1719+
/// Replace {@example ...} in API comments with the content of named file.
1720+
///
1721+
/// Syntax:
1722+
///
1723+
/// {@example PATH [region=NAME] [lang=NAME]}
1724+
///
1725+
/// where PATH and NAME are tokens _without_ whitespace; NAME can optionally be
1726+
/// quoted (use of quotes is for backwards compatibility and discouraged).
1727+
///
1728+
/// If PATH is `dir/file.ext` and region is `r` then we'll look for the file
1729+
/// named `dir/file-r.ext.md`, relative to the project root directory (of the
1730+
/// project for which the docs are being generated).
1731+
///
1732+
/// Examples:
1733+
///
1734+
/// {@example examples/angular/quickstart/web/main.dart}
1735+
/// {@example abc/def/xyz_component.dart region=template lang=html}
1736+
///
17221737
String _injectExamples(String rawdocs) {
1723-
if (rawdocs.contains('@example')) {
1724-
RegExp exp = new RegExp(r"{@example .+}");
1725-
Iterable<Match> matches = exp.allMatches(rawdocs);
1726-
var dirPath = this.package.packageMeta.dir.path;
1727-
for (var match in matches) {
1728-
var strings = match.group(0).split(' ');
1729-
var path = strings[1].replaceAll('/', Platform.pathSeparator);
1730-
if (path.contains(Platform.pathSeparator) &&
1731-
!path.startsWith(Platform.pathSeparator)) {
1732-
var file = new File(p.join(dirPath, 'examples', path));
1733-
if (file.existsSync()) {
1734-
// TODO(keertip):inject example
1735-
} else {
1736-
var filepath =
1737-
this.element.source.fullName.substring(dirPath.length + 1);
1738-
stdout.write(
1739-
'\nwarning: ${filepath}: file ${strings[1]} does not exist.');
1740-
}
1738+
final dirPath = this.package.packageMeta.dir.path;
1739+
RegExp exampleRE = new RegExp(r'{@example\s+([^}]+)}');
1740+
return rawdocs.replaceAllMapped(exampleRE, (match) {
1741+
var args = _getExampleArgs(match[1]);
1742+
var lang = args['lang'] ?? p.extension(args['src']);
1743+
1744+
var replacement = match[0]; // default to fully matched string.
1745+
1746+
var fragmentFile = new File(p.join(dirPath, args['file']));
1747+
if (fragmentFile.existsSync()) {
1748+
replacement = fragmentFile.readAsStringSync();
1749+
if (!lang.isEmpty) {
1750+
replacement = replacement.replaceFirst('```', '```$lang');
17411751
}
1752+
} else {
1753+
// TODO: is this the proper way to handle warnings?
1754+
var filePath = this.element.source.fullName.substring(dirPath.length + 1);
1755+
final msg = 'Warning: ${filePath}: @example file not found, $fragmentFile';
1756+
stderr.write(msg);
17421757
}
1743-
}
1744-
return rawdocs;
1758+
return replacement;
1759+
});
1760+
}
1761+
1762+
/// Helper for _injectExamples used to process @example arguments.
1763+
/// Returns a map of arguments. The first unnamed argument will have key 'src'.
1764+
/// The computed file path, constructed from 'src' and 'region' will have key
1765+
/// 'file'.
1766+
Map<String, String> _getExampleArgs(String argsAsString) {
1767+
// Extract PATH and return is under key 'src'
1768+
var endOfSrc = argsAsString.indexOf(' ');
1769+
if (endOfSrc < 0) endOfSrc = argsAsString.length;
1770+
var src = argsAsString.substring(0, endOfSrc);
1771+
src = src.replaceAll('/', Platform.pathSeparator);
1772+
final args = { 'src': src };
1773+
1774+
// Process remaining named arguments
1775+
var namedArgs = argsAsString.substring(endOfSrc);
1776+
// Arg value: allow optional quotes; warning: we still don't support whitespace.
1777+
RegExp keyValueRE = new RegExp('(\\w+)=[\'"]?(\\S*)[\'"]?');
1778+
Iterable<Match> matches = keyValueRE.allMatches(namedArgs);
1779+
matches.forEach((match) {
1780+
args[match[1]] = match[2];
1781+
});
1782+
1783+
// Compute 'file'
1784+
final fragExtension = '.md';
1785+
var file = src + fragExtension;
1786+
var region = args['region'] ?? '';
1787+
if (!region.isEmpty) {
1788+
var dir = p.dirname(src);
1789+
var basename = p.basenameWithoutExtension(src);
1790+
var ext = p.extension(src);
1791+
file = p.join(dir, '$basename-$region$ext$fragExtension');
1792+
}
1793+
args['file'] = file;
1794+
return args;
17451795
}
17461796
}
17471797

test/compare_output_test.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,9 @@ void main() {
133133
fail('dartdoc failed');
134134
}
135135

136-
if (!result.stdout
137-
.contains('core/pipes/ts/slice_pipe/slice_pipe_example.ts')) {
138-
fail('Did not process @example in comments');
136+
if (!result.stderr
137+
.contains(new RegExp(r'Warning:.*file-does-not-exist\.js'))) {
138+
fail('Warning missing for nonexistent @example: \nstdout: ${result.stdout} \nstderr: ${result.stderr}');
139139
}
140140
});
141141

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
A selection of dog flavors:
2+
3+
- beef
4+
- poultry
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Some text with **bold** remarks.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```
2+
<h1>Hello <b>World</b>!</h1>
3+
```

testing/test_package/lib/example.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,12 @@ class ConstantCat implements Cat {
194194
}
195195

196196
/// implements [Cat], [E]
197-
/// {@example core/pipes/ts/slice_pipe/slice_pipe_example.ts region='SlicePipe_list'}
197+
///
198+
/// {@example examples/dog_food}
199+
/// {@example examples/dog_food.txt region=meat}
200+
///
201+
/// {@example examples/test.dart region= lang=html}
202+
/// {@example examples/file-does-not-exist.js}
198203
class Dog implements Cat, E {
199204
String name;
200205

testing/test_package_docs/ex/Dog-class.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,12 @@ <h5><a href="ex/ex-library.html">ex</a></h5>
136136
<div class="col-xs-12 col-sm-9 col-md-8 main-content">
137137

138138
<section class="desc markdown">
139-
<p>implements <a href="ex/Cat-class.html">Cat</a>, <a href="ex/E-class.html">E</a>
140-
{@example core/pipes/ts/slice_pipe/slice_pipe_example.ts region='SlicePipe_list'}</p>
139+
<p>implements <a href="ex/Cat-class.html">Cat</a>, <a href="ex/E-class.html">E</a></p>
140+
<p>Some text with <strong>bold</strong> remarks.
141+
A selection of dog flavors:</p><ul><li>beef</li><li>poultry</li></ul>
142+
<pre class="language-html prettyprint"><code class="language-html">&lt;h1&gt;Hello &lt;b&gt;World&lt;/b&gt;!&lt;/h1&gt;
143+
</code></pre>
144+
<p>{@example examples/file-does-not-exist.js}</p>
141145
</section>
142146

143147
<section>

testing/test_package_docs/ex/ex-library.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,8 +350,7 @@ <h2>Classes</h2>
350350
<span class="name "><a href="ex/Dog-class.html">Dog</a></span>
351351
</dt>
352352
<dd>
353-
<p>implements <a href="ex/Cat-class.html">Cat</a>, <a href="ex/E-class.html">E</a>
354-
{@example core/pipes/ts/slice_pipe/slice_pipe_example.ts region='SlicePipe_list'}</p>
353+
<p>implements <a href="ex/Cat-class.html">Cat</a>, <a href="ex/E-class.html">E</a></p>
355354
</dd>
356355
<dt id="E">
357356
<span class="name "><a href="ex/E-class.html">E</a></span>

0 commit comments

Comments
 (0)