From 5e7e9127f0927729aef718377f8c177ca64cc991 Mon Sep 17 00:00:00 2001
From: Sam Rawlins
Date: Fri, 24 Jul 2020 07:19:24 -0700
Subject: [PATCH 1/3] Process directives after macro-processing; fixes #1945
---
lib/src/model/comment_processable.dart | 576 ++++++++++++++++++++++
lib/src/model/model_element.dart | 564 +--------------------
lib/src/model/package_graph.dart | 6 +-
test/model_special_cases_test.dart | 12 +
testing/test_package/bin/print_macro.dart | 10 +
testing/test_package/dartdoc_options.yaml | 3 +
testing/test_package/lib/example.dart | 28 +-
7 files changed, 642 insertions(+), 557 deletions(-)
create mode 100644 lib/src/model/comment_processable.dart
create mode 100644 testing/test_package/bin/print_macro.dart
diff --git a/lib/src/model/comment_processable.dart b/lib/src/model/comment_processable.dart
new file mode 100644
index 0000000000..faa656f859
--- /dev/null
+++ b/lib/src/model/comment_processable.dart
@@ -0,0 +1,576 @@
+import 'dart:io';
+
+import 'package:args/args.dart';
+import 'package:crypto/crypto.dart';
+import 'package:dartdoc/src/logging.dart';
+import 'package:dartdoc/src/model/model.dart';
+import 'package:dartdoc/src/render/model_element_renderer.dart';
+import 'package:dartdoc/src/utils.dart';
+import 'package:dartdoc/src/warnings.dart';
+
+import 'package:path/path.dart' as path;
+
+final _templatePattern = RegExp(
+ r'[ ]*{@template\s+(.+?)}([\s\S]+?){@endtemplate}[ ]*\n?',
+ multiLine: true);
+final _htmlPattern = RegExp(
+ r'[ ]*{@inject-html\s*}([\s\S]+?){@end-inject-html}[ ]*\n?',
+ multiLine: true);
+
+/// Matches all tool directives (even some invalid ones). This is so
+/// we can give good error messages if the directive is malformed, instead of
+/// just silently emitting it as-is.
+final _basicToolPattern = RegExp(
+ r'[ ]*{@tool\s+([^}]+)}\n?([\s\S]+?)\n?{@end-tool}[ ]*\n?',
+ multiLine: true);
+
+final _examplePattern = RegExp(r'{@example\s+([^}]+)}');
+
+/// Features for processing directives in a documentation comment.
+///
+/// [processCommentWithoutTools] and [processComment] are the primary
+/// entrypoints.
+mixin CommentProcessable on Documentable, Warnable, Locatable, SourceCodeMixin {
+ /// Process [documentationComment], performing various actions based on
+ /// `{@}`-style directives, except `{@tool}`, returning the processed result.
+ String processCommentWithoutTools(String documentationComment) {
+ var docs = stripComments(documentationComment);
+ docs = processCommentDirectives(docs);
+ return docs;
+ }
+
+ /// Process [documentationComment], performing various actions based on
+ /// `{@}`-style directives, returning the processed result.
+ Future processComment(String documentationComment) async {
+ var docs = stripComments(documentationComment);
+ // Must evaluate tools first, in case they insert any other directives.
+ docs = await _evaluateTools(docs);
+ docs = processCommentDirectives(docs);
+ return docs;
+ }
+
+ String processCommentDirectives(String docs) {
+ if (!docs.contains('{@')) {
+ return docs;
+ }
+ docs = _injectExamples(docs);
+ docs = _injectYouTube(docs);
+ docs = _injectAnimations(docs);
+ docs = _stripMacroTemplatesAndAddToIndex(docs);
+ docs = _stripHtmlAndAddToIndex(docs);
+ return docs;
+ }
+
+ String get _sourceFileName => element.source.fullName;
+
+ String get _fullyQualifiedNameWithoutLibrary =>
+ // Remember, periods are legal in library names.
+ fullyQualifiedName.replaceFirst('${library.fullyQualifiedName}.', '');
+
+ ModelElementRenderer get _modelElementRenderer =>
+ packageGraph.rendererFactory.modelElementRenderer;
+
+ /// Replace {@tool ...}{@end-tool} in API comments with the
+ /// output of an external tool.
+ ///
+ /// Looks for tools invocations, looks up their bound executables in the
+ /// options, and executes them with the source comment material as input,
+ /// returning the output of the tool. If a named tool isn't configured in the
+ /// options file, then it will not be executed, and dartdoc will quit with an
+ /// error.
+ ///
+ /// Tool command line arguments are passed to the tool, with the token
+ /// `$INPUT` replaced with the absolute path to a temporary file containing
+ /// the content for the tool to read and produce output from. If the tool
+ /// doesn't need any input, then no `$INPUT` is needed.
+ ///
+ /// Nested tool directives will not be evaluated, but tools may generate other
+ /// directives in their output and those will be evaluated.
+ ///
+ /// Syntax:
+ ///
+ /// {@tool TOOL [Tool arguments]}
+ /// Content to send to tool.
+ /// {@end-tool}
+ ///
+ /// Examples:
+ ///
+ /// In `dart_options.yaml`:
+ ///
+ /// ```yaml
+ /// dartdoc:
+ /// tools:
+ /// # Prefixes the given input with "## "
+ /// # Path is relative to project root.
+ /// prefix: "bin/prefix.dart"
+ /// # Prints the date
+ /// date: "/bin/date"
+ /// ```
+ ///
+ /// In code:
+ ///
+ /// _This:_
+ ///
+ /// {@tool prefix $INPUT}
+ /// Content to send to tool.
+ /// {@end-tool}
+ /// {@tool date --iso-8601=minutes --utc}
+ /// {@end-tool}
+ ///
+ /// _Produces:_
+ ///
+ /// ## Content to send to tool.
+ /// 2018-09-18T21:15+00:00
+ Future _evaluateTools(String rawDocs) async {
+ if (!config.allowTools) {
+ return rawDocs;
+ }
+ var invocationIndex = 0;
+ return await _replaceAllMappedAsync(rawDocs, _basicToolPattern,
+ (basicMatch) async {
+ var args = _splitUpQuotedArgs(basicMatch[1]).toList();
+ // Tool name must come first.
+ if (args.isEmpty) {
+ warn(PackageWarning.toolError,
+ message: 'Must specify a tool to execute for the @tool directive.');
+ return Future.value('');
+ }
+ // Count the number of invocations of tools in this dartdoc block,
+ // so that tools can differentiate different blocks from each other.
+ invocationIndex++;
+ return await config.tools.runner.run(
+ args,
+ (String message) async =>
+ warn(PackageWarning.toolError, message: message),
+ content: basicMatch[2],
+ environment: {
+ 'SOURCE_LINE': characterLocation?.lineNumber.toString(),
+ 'SOURCE_COLUMN': characterLocation?.columnNumber.toString(),
+ 'SOURCE_PATH':
+ (_sourceFileName == null || package?.packagePath == null)
+ ? null
+ : path.relative(_sourceFileName, from: package.packagePath),
+ 'PACKAGE_PATH': package?.packagePath,
+ 'PACKAGE_NAME': package?.name,
+ 'LIBRARY_NAME': library?.fullyQualifiedName,
+ 'ELEMENT_NAME': _fullyQualifiedNameWithoutLibrary,
+ 'INVOCATION_INDEX': invocationIndex.toString(),
+ 'PACKAGE_INVOCATION_INDEX':
+ (package.toolInvocationIndex++).toString(),
+ }..removeWhere((key, value) => value == null));
+ });
+ }
+
+ /// Replace {@example ...} in API comments with the content of named file.
+ ///
+ /// Syntax:
+ ///
+ /// {@example PATH [region=NAME] [lang=NAME]}
+ ///
+ /// If PATH is `dir/file.ext` and region is `r` then we'll look for the file
+ /// named `dir/file-r.ext.md`, relative to the project root directory of the
+ /// project for which the docs are being generated.
+ ///
+ /// Examples: (escaped in this comment to show literal values in dartdoc's
+ /// dartdoc)
+ ///
+ /// {@example examples/angular/quickstart/web/main.dart}
+ /// {@example abc/def/xyz_component.dart region=template lang=html}
+ ///
+ String _injectExamples(String rawdocs) {
+ final dirPath = package.packageMeta.dir.path;
+ return rawdocs.replaceAllMapped(_examplePattern, (match) {
+ var args = _getExampleArgs(match[1]);
+ if (args == null) {
+ // Already warned about an invalid parameter if this happens.
+ return '';
+ }
+ var lang =
+ args['lang'] ?? path.extension(args['src']).replaceFirst('.', '');
+
+ var replacement = match[0]; // default to fully matched string.
+
+ var fragmentFile = File(path.join(dirPath, args['file']));
+ if (fragmentFile.existsSync()) {
+ replacement = fragmentFile.readAsStringSync();
+ if (lang.isNotEmpty) {
+ replacement = replacement.replaceFirst('```', '```$lang');
+ }
+ } else {
+ // TODO(jcollins-g): move this to Package.warn system
+ var filePath = element.source.fullName.substring(dirPath.length + 1);
+
+ logWarning(
+ 'warning: ${filePath}: @example file not found, ${fragmentFile.path}');
+ }
+ return replacement;
+ });
+ }
+
+ /// Helper for [_injectExamples] used to process `@example` arguments.
+ ///
+ /// Returns a map of arguments. The first unnamed argument will have key
+ /// 'src'. The computed file path, constructed from 'src' and 'region' will
+ /// have key 'file'.
+ Map _getExampleArgs(String argsAsString) {
+ var parser = ArgParser();
+ parser.addOption('lang');
+ parser.addOption('region');
+ var results = _parseArgs(argsAsString, parser, 'example');
+ if (results == null) {
+ return null;
+ }
+
+ // Extract PATH and fix the path separators.
+ var src = results.rest.isEmpty
+ ? ''
+ : results.rest.first.replaceAll('/', Platform.pathSeparator);
+ var args = {
+ 'src': src,
+ 'lang': results['lang'],
+ 'region': results['region'] ?? '',
+ };
+
+ // Compute 'file' from region and src.
+ final fragExtension = '.md';
+ var file = src + fragExtension;
+ var region = args['region'] ?? '';
+ if (region.isNotEmpty) {
+ var dir = path.dirname(src);
+ var basename = path.basenameWithoutExtension(src);
+ var ext = path.extension(src);
+ file = path.join(dir, '$basename-$region$ext$fragExtension');
+ }
+ args['file'] = config.examplePathPrefix == null
+ ? file
+ : path.join(config.examplePathPrefix, file);
+ return args;
+ }
+
+ /// Matches all youtube directives (even some invalid ones). This is so
+ /// we can give good error messages if the directive is malformed, instead of
+ /// just silently emitting it as-is.
+ static final _basicYouTubePattern = RegExp(r'''{@youtube\s+([^}]+)}''');
+
+ /// Matches YouTube IDs from supported YouTube URLs.
+ static final _validYouTubeUrlPattern =
+ RegExp('https://www\.youtube\.com/watch\\?v=([^&]+)\$');
+
+ /// Replace {@youtube ...} in API comments with some HTML to embed
+ /// a YouTube video.
+ ///
+ /// Syntax:
+ ///
+ /// {@youtube WIDTH HEIGHT URL}
+ ///
+ /// Example:
+ ///
+ /// {@youtube 560 315 https://www.youtube.com/watch?v=oHg5SJYRHA0}
+ ///
+ /// Which will embed a YouTube player into the page that plays the specified
+ /// video.
+ ///
+ /// The width and height must be positive integers specifying the dimensions
+ /// of the video in pixels. The height and width are used to calculate the
+ /// aspect ratio of the video; the video is always rendered to take up all
+ /// available horizontal space to accommodate different screen sizes on
+ /// desktop and mobile.
+ ///
+ /// The video URL must have the following format:
+ /// https://www.youtube.com/watch?v=oHg5SJYRHA0. This format can usually be
+ /// found in the address bar of the browser when viewing a YouTube video.
+ String _injectYouTube(String rawDocs) {
+ return rawDocs.replaceAllMapped(_basicYouTubePattern, (basicMatch) {
+ var parser = ArgParser();
+ var args = _parseArgs(basicMatch[1], parser, 'youtube');
+ if (args == null) {
+ // Already warned about an invalid parameter if this happens.
+ return '';
+ }
+ var positionalArgs = args.rest.sublist(0);
+ if (positionalArgs.length != 3) {
+ warn(PackageWarning.invalidParameter,
+ message: 'Invalid @youtube directive, "${basicMatch[0]}"\n'
+ 'YouTube directives must be of the form "{@youtube WIDTH '
+ 'HEIGHT URL}"');
+ return '';
+ }
+
+ var width = int.tryParse(positionalArgs[0]);
+ if (width == null || width <= 0) {
+ warn(PackageWarning.invalidParameter,
+ message: 'A @youtube directive has an invalid width, '
+ '"${positionalArgs[0]}". The width must be a positive '
+ 'integer.');
+ }
+
+ var height = int.tryParse(positionalArgs[1]);
+ if (height == null || height <= 0) {
+ warn(PackageWarning.invalidParameter,
+ message: 'A @youtube directive has an invalid height, '
+ '"${positionalArgs[1]}". The height must be a positive '
+ 'integer.');
+ }
+
+ var url = _validYouTubeUrlPattern.firstMatch(positionalArgs[2]);
+ if (url == null) {
+ warn(PackageWarning.invalidParameter,
+ message: 'A @youtube directive has an invalid URL: '
+ '"${positionalArgs[2]}". Supported YouTube URLs have the '
+ 'following format: '
+ 'https://www.youtube.com/watch?v=oHg5SJYRHA0.');
+ return '';
+ }
+ var youTubeId = url.group(url.groupCount);
+ var aspectRatio = (height / width * 100).toStringAsFixed(2);
+
+ return _modelElementRenderer.renderYoutubeUrl(youTubeId, aspectRatio);
+ });
+ }
+
+ /// Matches all animation directives (even some invalid ones). This is so
+ /// we can give good error messages if the directive is malformed, instead of
+ /// just silently emitting it as-is.
+ final _basicAnimationPattern = RegExp(r'''{@animation\s+([^}]+)}''');
+
+ /// Matches valid javascript identifiers.
+ final _validIdPattern = RegExp(r'^[a-zA-Z_]\w*$');
+
+ /// Replace {@animation ...} in API comments with some HTML to
+ /// manage an MPEG 4 video as an animation.
+ ///
+ /// Syntax:
+ ///
+ /// {@animation WIDTH HEIGHT URL [id=ID]}
+ ///
+ /// Example:
+ ///
+ /// {@animation 300 300 https://example.com/path/to/video.mp4 id="my_video"}
+ ///
+ /// Which will render the HTML necessary for embedding a simple click-to-play
+ /// HTML5 video player with no controls that has an HTML id of "my_video".
+ ///
+ /// The optional ID should be a unique id that is a valid JavaScript
+ /// identifier, and will be used as the id for the video tag. If no ID is
+ /// supplied, then a unique identifier (starting with "animation_") will be
+ /// generated.
+ ///
+ /// The width and height must be integers specifying the dimensions of the
+ /// video file in pixels.
+ String _injectAnimations(String rawDocs) {
+ // Make sure we have a set to keep track of used IDs for this href.
+ package.usedAnimationIdsByHref[href] ??= {};
+
+ String getUniqueId(String base) {
+ var animationIdCount = 1;
+ var id = '$base$animationIdCount';
+ // We check for duplicate IDs so that we make sure not to collide with
+ // user-supplied ids on the same page.
+ while (package.usedAnimationIdsByHref[href].contains(id)) {
+ animationIdCount++;
+ id = '$base$animationIdCount';
+ }
+ return id;
+ }
+
+ return rawDocs.replaceAllMapped(_basicAnimationPattern, (basicMatch) {
+ var parser = ArgParser();
+ parser.addOption('id');
+ var args = _parseArgs(basicMatch[1], parser, 'animation');
+ if (args == null) {
+ // Already warned about an invalid parameter if this happens.
+ return '';
+ }
+ final positionalArgs = args.rest.sublist(0);
+ String uniqueId;
+ var wasDeprecated = false;
+ if (positionalArgs.length == 4) {
+ // Supports the original form of the animation tag for backward
+ // compatibility.
+ uniqueId = positionalArgs.removeAt(0);
+ wasDeprecated = true;
+ } else if (positionalArgs.length == 3) {
+ uniqueId = args['id'] ?? getUniqueId('animation_');
+ } else {
+ warn(PackageWarning.invalidParameter,
+ message: 'Invalid @animation directive, "${basicMatch[0]}"\n'
+ 'Animation directives must be of the form "{@animation WIDTH '
+ 'HEIGHT URL [id=ID]}"');
+ return '';
+ }
+
+ if (!_validIdPattern.hasMatch(uniqueId)) {
+ warn(PackageWarning.invalidParameter,
+ message: 'An animation has an invalid identifier, "$uniqueId". The '
+ 'identifier can only contain letters, numbers and underscores, '
+ 'and must not begin with a number.');
+ return '';
+ }
+ if (package.usedAnimationIdsByHref[href].contains(uniqueId)) {
+ warn(PackageWarning.invalidParameter,
+ message: 'An animation has a non-unique identifier, "$uniqueId". '
+ 'Animation identifiers must be unique.');
+ return '';
+ }
+ package.usedAnimationIdsByHref[href].add(uniqueId);
+
+ int width;
+ try {
+ width = int.parse(positionalArgs[0]);
+ } on FormatException {
+ warn(PackageWarning.invalidParameter,
+ message: 'An animation has an invalid width ($uniqueId), '
+ '"${positionalArgs[0]}". The width must be an integer.');
+ return '';
+ }
+
+ int height;
+ try {
+ height = int.parse(positionalArgs[1]);
+ } on FormatException {
+ warn(PackageWarning.invalidParameter,
+ message: 'An animation has an invalid height ($uniqueId), '
+ '"${positionalArgs[1]}". The height must be an integer.');
+ return '';
+ }
+
+ Uri movieUrl;
+ try {
+ movieUrl = Uri.parse(positionalArgs[2]);
+ } on FormatException catch (e) {
+ warn(PackageWarning.invalidParameter,
+ message: 'An animation URL could not be parsed ($uniqueId): '
+ '${positionalArgs[2]}\n$e');
+ return '';
+ }
+ var overlayId = '${uniqueId}_play_button_';
+
+ // Only warn about deprecation if some other warning didn't occur.
+ if (wasDeprecated) {
+ warn(PackageWarning.deprecated,
+ message:
+ 'Deprecated form of @animation directive, "${basicMatch[0]}"\n'
+ 'Animation directives are now of the form "{@animation '
+ 'WIDTH HEIGHT URL [id=ID]}" (id is an optional '
+ 'parameter)');
+ }
+
+ return _modelElementRenderer.renderAnimation(
+ uniqueId, width, height, movieUrl, overlayId);
+ });
+ }
+
+ /// Parse and remove {@template ...} in API comments and store them
+ /// in the index on the package.
+ ///
+ /// Syntax:
+ ///
+ /// {@template NAME}
+ /// The contents of the macro
+ /// {@endtemplate}
+ ///
+ String _stripMacroTemplatesAndAddToIndex(String rawDocs) {
+ return rawDocs.replaceAllMapped(_templatePattern, (match) {
+ var name = match[1].trim();
+ var content = match[2].trim();
+ packageGraph.addMacro(name, content);
+ return '{@macro $name}';
+ });
+ }
+
+ /// Parse and remove {@inject-html ...} in API comments and store
+ /// them in the index on the package, replacing them with a SHA1 hash of the
+ /// contents, where the HTML will be re-injected after Markdown processing of
+ /// the rest of the text is complete.
+ ///
+ /// Syntax:
+ ///
+ /// {@inject-html}
+ /// The HTML to inject.
+ /// {@end-inject-html}
+ ///
+ String _stripHtmlAndAddToIndex(String rawDocs) {
+ if (!config.injectHtml) return rawDocs;
+ return rawDocs.replaceAllMapped(_htmlPattern, (match) {
+ var fragment = match[1];
+ var digest = sha1.convert(fragment.codeUnits).toString();
+ packageGraph.addHtmlFragment(digest, fragment);
+ // The newlines are so that Markdown will pass this through without
+ // touching it.
+ return '\n$digest\n';
+ });
+ }
+
+ /// Helper to process arguments given as a (possibly quoted) string.
+ ///
+ /// First, this will split the given [argsAsString] into separate arguments
+ /// with [_splitUpQuotedArgs] it then parses the resulting argument list
+ /// normally with [argParser] and returns the result.
+ ArgResults _parseArgs(
+ String argsAsString, ArgParser argParser, String directiveName) {
+ var args = _splitUpQuotedArgs(argsAsString, convertToArgs: true);
+ try {
+ return argParser.parse(args);
+ } on ArgParserException catch (e) {
+ warn(PackageWarning.invalidParameter,
+ message: 'The {@$directiveName ...} directive was called with '
+ 'invalid parameters. $e');
+ return null;
+ }
+ }
+
+ static Future _replaceAllMappedAsync(String string, Pattern exp,
+ Future Function(Match match) replace) async {
+ var replaced = StringBuffer();
+ var currentIndex = 0;
+ for (var match in exp.allMatches(string)) {
+ var prefix = match.input.substring(currentIndex, match.start);
+ currentIndex = match.end;
+ replaced..write(prefix)..write(await replace(match));
+ }
+ replaced.write(string.substring(currentIndex));
+ return replaced.toString();
+ }
+
+ /// Regexp to take care of splitting arguments, and handling the quotes
+ /// around arguments, if any.
+ ///
+ /// Match group 1 is the "foo=" (or "--foo=") part of the option, if any.
+ /// Match group 2 contains the quote character used (which is discarded).
+ /// Match group 3 is a quoted arg, if any, without the quotes.
+ /// Match group 4 is the unquoted arg, if any.
+ static final RegExp _argPattern = RegExp(r'([a-zA-Z\-_0-9]+=)?' // option name
+ r'(?:' // Start a new non-capture group for the two possibilities.
+ r'''(["'])((?:\\{2})*|(?:.*?[^\\](?:\\{2})*))\2|''' // with quotes.
+ r'([^ ]+))'); // without quotes.
+
+ /// Helper to process arguments given as a (possibly quoted) string.
+ ///
+ /// First, this will split the given [argsAsString] into separate arguments,
+ /// taking any quoting (either ' or " are accepted) into account, including
+ /// handling backslash-escaped quotes.
+ ///
+ /// Then, it will prepend "--" to any args that start with an identifier
+ /// followed by an equals sign, allowing the argument parser to treat any
+ /// "foo=bar" argument as "--foo=bar". It does handle quoted args like
+ /// "foo='bar baz'" too, returning just bar (without quotes) for the foo
+ /// value.
+ static Iterable _splitUpQuotedArgs(String argsAsString,
+ {bool convertToArgs = false}) {
+ final Iterable matches = _argPattern.allMatches(argsAsString);
+ // Remove quotes around args, and if [convertToArgs] is true, then for any
+ // args that look like assignments (start with valid option names followed
+ // by an equals sign), add a "--" in front so that they parse as options.
+ return matches.map((Match match) {
+ var option = '';
+ if (convertToArgs && match[1] != null && !match[1].startsWith('-')) {
+ option = '--';
+ }
+ if (match[2] != null) {
+ // This arg has quotes, so strip them.
+ return '$option${match[1] ?? ''}${match[3] ?? ''}${match[4] ?? ''}';
+ }
+ return '$option${match[0]}';
+ });
+ }
+}
diff --git a/lib/src/model/model_element.dart b/lib/src/model/model_element.dart
index 953c3159a3..c19737b0f1 100644
--- a/lib/src/model/model_element.dart
+++ b/lib/src/model/model_element.dart
@@ -8,19 +8,16 @@ library dartdoc.models;
import 'dart:async';
import 'dart:collection' show UnmodifiableListView;
import 'dart:convert';
-import 'dart:io';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/member.dart'
show ExecutableMember, Member, ParameterMember;
-import 'package:args/args.dart';
import 'package:collection/collection.dart';
-import 'package:crypto/crypto.dart';
import 'package:dartdoc/src/dartdoc_options.dart';
import 'package:dartdoc/src/element_type.dart';
-import 'package:dartdoc/src/logging.dart';
+import 'package:dartdoc/src/model/comment_processable.dart';
import 'package:dartdoc/src/model/feature_set.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/model_utils.dart' as utils;
@@ -29,7 +26,6 @@ import 'package:dartdoc/src/render/parameter_renderer.dart';
import 'package:dartdoc/src/render/source_code_renderer.dart';
import 'package:dartdoc/src/source_linker.dart';
import 'package:dartdoc/src/tuple.dart';
-import 'package:dartdoc/src/utils.dart';
import 'package:dartdoc/src/warnings.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
@@ -71,33 +67,8 @@ int byFeatureOrdering(String a, String b) {
/// fragment.
final needsPrecacheRegExp = RegExp(r'{@(template|tool|inject-html)');
-final templateRegExp = RegExp(
- r'[ ]*{@template\s+(.+?)}([\s\S]+?){@endtemplate}[ ]*\n?',
- multiLine: true);
-final htmlRegExp = RegExp(
- r'[ ]*{@inject-html\s*}([\s\S]+?){@end-inject-html}[ ]*\n?',
- multiLine: true);
final htmlInjectRegExp = RegExp(r'([a-f0-9]+)');
-// Matches all tool directives (even some invalid ones). This is so
-// we can give good error messages if the directive is malformed, instead of
-// just silently emitting it as-is.
-final basicToolRegExp = RegExp(
- r'[ ]*{@tool\s+([^}]+)}\n?([\s\S]+?)\n?{@end-tool}[ ]*\n?',
- multiLine: true);
-
-/// Regexp to take care of splitting arguments, and handling the quotes
-/// around arguments, if any.
-///
-/// Match group 1 is the "foo=" (or "--foo=") part of the option, if any.
-/// Match group 2 contains the quote character used (which is discarded).
-/// Match group 3 is a quoted arg, if any, without the quotes.
-/// Match group 4 is the unquoted arg, if any.
-final RegExp argMatcher = RegExp(r'([a-zA-Z\-_0-9]+=)?' // option name
- r'(?:' // Start a new non-capture group for the two possibilities.
- r'''(["'])((?:\\{2})*|(?:.*?[^\\](?:\\{2})*))\2|''' // with quotes.
- r'([^ ]+))'); // without quotes.
-
final macroRegExp = RegExp(r'{@macro\s+([^}]+)}');
// TODO(jcollins-g): Implement resolution per ECMA-408 4th edition, page 39 #22.
@@ -158,7 +129,8 @@ abstract class ModelElement extends Canonicalization
Nameable,
SourceCodeMixin,
Indexable,
- FeatureSet
+ FeatureSet,
+ CommentProcessable
implements Comparable, Documentable {
final Element _element;
@@ -172,9 +144,6 @@ abstract class ModelElement extends Canonicalization
UnmodifiableListView _parameters;
String _linkedName;
- String _fullyQualifiedName;
- String _fullyQualifiedNameWithoutLibrary;
-
// TODO(jcollins-g): make _originalMember optional after dart-lang/sdk#15101
// is fixed.
ModelElement(
@@ -599,22 +568,19 @@ abstract class ModelElement extends Canonicalization
/// to find docs, if the current class doesn't have docs
/// for this element.
List get computeDocumentationFrom {
- List docFrom;
-
if (documentationComment == null &&
- canOverride() &&
+ _canOverride &&
this is Inheritable &&
(this as Inheritable).overriddenElement != null) {
- docFrom = (this as Inheritable).overriddenElement.documentationFrom;
+ return (this as Inheritable).overriddenElement.documentationFrom;
} else if (this is Inheritable && (this as Inheritable).isInherited) {
var thisInheritable = (this as Inheritable);
var fromThis = ModelElement.fromElement(
element, thisInheritable.definingEnclosingContainer.packageGraph);
- docFrom = fromThis.documentationFrom;
+ return fromThis.documentationFrom;
} else {
- docFrom = [this];
+ return [this];
}
- return docFrom;
}
String _buildDocumentationLocal() => _buildDocumentationBaseSync();
@@ -633,12 +599,7 @@ abstract class ModelElement extends Canonicalization
if (config.dropTextFrom.contains(element.library.name)) {
_rawDocs = '';
} else {
- _rawDocs = documentationComment ?? '';
- _rawDocs = stripComments(_rawDocs) ?? '';
- _rawDocs = _injectExamples(_rawDocs);
- _rawDocs = _injectYouTube(_rawDocs);
- _rawDocs = _injectAnimations(_rawDocs);
- _rawDocs = _stripHtmlAndAddToIndex(_rawDocs);
+ _rawDocs = processCommentWithoutTools(documentationComment ?? '');
}
_rawDocs = buildDocumentationAddition(_rawDocs);
return _rawDocs;
@@ -653,15 +614,7 @@ abstract class ModelElement extends Canonicalization
if (config.dropTextFrom.contains(element.library.name)) {
_rawDocs = '';
} else {
- _rawDocs = documentationComment ?? '';
- _rawDocs = stripComments(_rawDocs) ?? '';
- // Must evaluate tools first, in case they insert any other directives.
- _rawDocs = await _evaluateTools(_rawDocs);
- _rawDocs = _injectExamples(_rawDocs);
- _rawDocs = _injectYouTube(_rawDocs);
- _rawDocs = _injectAnimations(_rawDocs);
- _rawDocs = _stripMacroTemplatesAndAddToIndex(_rawDocs);
- _rawDocs = _stripHtmlAndAddToIndex(_rawDocs);
+ _rawDocs = await processComment(documentationComment ?? '');
}
_rawDocs = buildDocumentationAddition(_rawDocs);
return _rawDocs;
@@ -688,8 +641,8 @@ abstract class ModelElement extends Canonicalization
Library _canonicalLibrary;
- // _canonicalLibrary can be null so we can't check against null to see whether
- // we tried to compute it before.
+ // [_canonicalLibrary] can be null so we can't check against null to see
+ // whether we tried to compute it before.
bool _canonicalLibraryIsSet = false;
@override
@@ -846,6 +799,8 @@ abstract class ModelElement extends Canonicalization
String get filePath;
+ String _fullyQualifiedName;
+
/// Returns the fully qualified name.
///
/// For example: libraryName.className.methodName
@@ -854,6 +809,7 @@ abstract class ModelElement extends Canonicalization
return (_fullyQualifiedName ??= _buildFullyQualifiedName());
}
+ String _fullyQualifiedNameWithoutLibrary;
String get fullyQualifiedNameWithoutLibrary {
// Remember, periods are legal in library names.
_fullyQualifiedNameWithoutLibrary ??=
@@ -1153,7 +1109,7 @@ abstract class ModelElement extends Canonicalization
_sourceCodeRenderer.renderSourceCode(super.sourceCode);
}
- bool canOverride() =>
+ bool get _canOverride =>
element is ClassMemberElement || element is PropertyAccessorElement;
@override
@@ -1198,369 +1154,6 @@ abstract class ModelElement extends Canonicalization
return _modelElementRenderer.renderLinkedName(this);
}
- /// Replace {@example ...} in API comments with the content of named file.
- ///
- /// Syntax:
- ///
- /// {@example PATH [region=NAME] [lang=NAME]}
- ///
- /// If PATH is `dir/file.ext` and region is `r` then we'll look for the file
- /// named `dir/file-r.ext.md`, relative to the project root directory of the
- /// project for which the docs are being generated.
- ///
- /// Examples: (escaped in this comment to show literal values in dartdoc's
- /// dartdoc)
- ///
- /// {@example examples/angular/quickstart/web/main.dart}
- /// {@example abc/def/xyz_component.dart region=template lang=html}
- ///
- String _injectExamples(String rawdocs) {
- final dirPath = package.packageMeta.dir.path;
- var exampleRE = RegExp(r'{@example\s+([^}]+)}');
- return rawdocs.replaceAllMapped(exampleRE, (match) {
- var args = _getExampleArgs(match[1]);
- if (args == null) {
- // Already warned about an invalid parameter if this happens.
- return '';
- }
- var lang =
- args['lang'] ?? path.extension(args['src']).replaceFirst('.', '');
-
- var replacement = match[0]; // default to fully matched string.
-
- var fragmentFile = File(path.join(dirPath, args['file']));
- if (fragmentFile.existsSync()) {
- replacement = fragmentFile.readAsStringSync();
- if (lang.isNotEmpty) {
- replacement = replacement.replaceFirst('```', '```$lang');
- }
- } else {
- // TODO(jcollins-g): move this to Package.warn system
- var filePath = element.source.fullName.substring(dirPath.length + 1);
-
- logWarning(
- 'warning: ${filePath}: @example file not found, ${fragmentFile.path}');
- }
- return replacement;
- });
- }
-
- static Future _replaceAllMappedAsync(String string, Pattern exp,
- Future Function(Match match) replace) async {
- var replaced = StringBuffer();
- var currentIndex = 0;
- for (var match in exp.allMatches(string)) {
- var prefix = match.input.substring(currentIndex, match.start);
- currentIndex = match.end;
- replaced..write(prefix)..write(await replace(match));
- }
- replaced.write(string.substring(currentIndex));
- return replaced.toString();
- }
-
- /// Replace {@tool ...}{@end-tool} in API comments with the
- /// output of an external tool.
- ///
- /// Looks for tools invocations, looks up their bound executables in the
- /// options, and executes them with the source comment material as input,
- /// returning the output of the tool. If a named tool isn't configured in the
- /// options file, then it will not be executed, and dartdoc will quit with an
- /// error.
- ///
- /// Tool command line arguments are passed to the tool, with the token
- /// `$INPUT` replaced with the absolute path to a temporary file containing
- /// the content for the tool to read and produce output from. If the tool
- /// doesn't need any input, then no `$INPUT` is needed.
- ///
- /// Nested tool directives will not be evaluated, but tools may generate other
- /// directives in their output and those will be evaluated.
- ///
- /// Syntax:
- ///
- /// {@tool TOOL [Tool arguments]}
- /// Content to send to tool.
- /// {@end-tool}
- ///
- /// Examples:
- ///
- /// In `dart_options.yaml`:
- ///
- /// ```yaml
- /// dartdoc:
- /// tools:
- /// # Prefixes the given input with "## "
- /// # Path is relative to project root.
- /// prefix: "bin/prefix.dart"
- /// # Prints the date
- /// date: "/bin/date"
- /// ```
- ///
- /// In code:
- ///
- /// _This:_
- ///
- /// {@tool prefix $INPUT}
- /// Content to send to tool.
- /// {@end-tool}
- /// {@tool date --iso-8601=minutes --utc}
- /// {@end-tool}
- ///
- /// _Produces:_
- ///
- /// ## Content to send to tool.
- /// 2018-09-18T21:15+00:00
- Future _evaluateTools(String rawDocs) async {
- if (config.allowTools) {
- var invocationIndex = 0;
- return await _replaceAllMappedAsync(rawDocs, basicToolRegExp,
- (basicMatch) async {
- var args = _splitUpQuotedArgs(basicMatch[1]).toList();
- // Tool name must come first.
- if (args.isEmpty) {
- warn(PackageWarning.toolError,
- message:
- 'Must specify a tool to execute for the @tool directive.');
- return Future.value('');
- }
- // Count the number of invocations of tools in this dartdoc block,
- // so that tools can differentiate different blocks from each other.
- invocationIndex++;
- return await config.tools.runner.run(
- args,
- (String message) async =>
- warn(PackageWarning.toolError, message: message),
- content: basicMatch[2],
- environment: {
- 'SOURCE_LINE': characterLocation?.lineNumber.toString(),
- 'SOURCE_COLUMN': characterLocation?.columnNumber.toString(),
- 'SOURCE_PATH': (sourceFileName == null ||
- package?.packagePath == null)
- ? null
- : path.relative(sourceFileName, from: package.packagePath),
- 'PACKAGE_PATH': package?.packagePath,
- 'PACKAGE_NAME': package?.name,
- 'LIBRARY_NAME': library?.fullyQualifiedName,
- 'ELEMENT_NAME': fullyQualifiedNameWithoutLibrary,
- 'INVOCATION_INDEX': invocationIndex.toString(),
- 'PACKAGE_INVOCATION_INDEX':
- (package.toolInvocationIndex++).toString(),
- }..removeWhere((key, value) => value == null));
- });
- } else {
- return rawDocs;
- }
- }
-
- /// Replace {@youtube ...} in API comments with some HTML to embed
- /// a YouTube video.
- ///
- /// Syntax:
- ///
- /// {@youtube WIDTH HEIGHT URL}
- ///
- /// Example:
- ///
- /// {@youtube 560 315 https://www.youtube.com/watch?v=oHg5SJYRHA0}
- ///
- /// Which will embed a YouTube player into the page that plays the specified
- /// video.
- ///
- /// The width and height must be positive integers specifying the dimensions
- /// of the video in pixels. The height and width are used to calculate the
- /// aspect ratio of the video; the video is always rendered to take up all
- /// available horizontal space to accommodate different screen sizes on
- /// desktop and mobile.
- ///
- /// The video URL must have the following format:
- /// https://www.youtube.com/watch?v=oHg5SJYRHA0. This format can usually be
- /// found in the address bar of the browser when viewing a YouTube video.
- String _injectYouTube(String rawDocs) {
- // Matches all youtube directives (even some invalid ones). This is so
- // we can give good error messages if the directive is malformed, instead of
- // just silently emitting it as-is.
- var basicAnimationRegExp = RegExp(r'''{@youtube\s+([^}]+)}''');
-
- // Matches YouTube IDs from supported YouTube URLs.
- var validYouTubeUrlRegExp =
- RegExp('https://www\.youtube\.com/watch\\?v=([^&]+)\$');
-
- return rawDocs.replaceAllMapped(basicAnimationRegExp, (basicMatch) {
- var parser = ArgParser();
- var args = _parseArgs(basicMatch[1], parser, 'youtube');
- if (args == null) {
- // Already warned about an invalid parameter if this happens.
- return '';
- }
- var positionalArgs = args.rest.sublist(0);
- if (positionalArgs.length != 3) {
- warn(PackageWarning.invalidParameter,
- message: 'Invalid @youtube directive, "${basicMatch[0]}"\n'
- 'YouTube directives must be of the form "{@youtube WIDTH '
- 'HEIGHT URL}"');
- return '';
- }
-
- var width = int.tryParse(positionalArgs[0]);
- if (width == null || width <= 0) {
- warn(PackageWarning.invalidParameter,
- message: 'A @youtube directive has an invalid width, '
- '"${positionalArgs[0]}". The width must be a positive integer.');
- }
-
- var height = int.tryParse(positionalArgs[1]);
- if (height == null || height <= 0) {
- warn(PackageWarning.invalidParameter,
- message: 'A @youtube directive has an invalid height, '
- '"${positionalArgs[1]}". The height must be a positive integer.');
- }
-
- var url = validYouTubeUrlRegExp.firstMatch(positionalArgs[2]);
- if (url == null) {
- warn(PackageWarning.invalidParameter,
- message: 'A @youtube directive has an invalid URL: '
- '"${positionalArgs[2]}". Supported YouTube URLs have the '
- 'following format: https://www.youtube.com/watch?v=oHg5SJYRHA0.');
- return '';
- }
- var youTubeId = url.group(url.groupCount);
- var aspectRatio = (height / width * 100).toStringAsFixed(2);
-
- return _modelElementRenderer.renderYoutubeUrl(youTubeId, aspectRatio);
- });
- }
-
- /// Replace {@animation ...} in API comments with some HTML to manage an
- /// MPEG 4 video as an animation.
- ///
- /// Syntax:
- ///
- /// {@animation WIDTH HEIGHT URL [id=ID]}
- ///
- /// Example:
- ///
- /// {@animation 300 300 https://example.com/path/to/video.mp4 id="my_video"}
- ///
- /// Which will render the HTML necessary for embedding a simple click-to-play
- /// HTML5 video player with no controls that has an HTML id of "my_video".
- ///
- /// The optional ID should be a unique id that is a valid JavaScript
- /// identifier, and will be used as the id for the video tag. If no ID is
- /// supplied, then a unique identifier (starting with "animation_") will be
- /// generated.
- ///
- /// The width and height must be integers specifying the dimensions of the
- /// video file in pixels.
- String _injectAnimations(String rawDocs) {
- // Matches all animation directives (even some invalid ones). This is so
- // we can give good error messages if the directive is malformed, instead of
- // just silently emitting it as-is.
- var basicAnimationRegExp = RegExp(r'''{@animation\s+([^}]+)}''');
-
- // Matches valid javascript identifiers.
- var validIdRegExp = RegExp(r'^[a-zA-Z_]\w*$');
-
- // Make sure we have a set to keep track of used IDs for this href.
- package.usedAnimationIdsByHref[href] ??= {};
-
- String getUniqueId(String base) {
- var animationIdCount = 1;
- var id = '$base$animationIdCount';
- // We check for duplicate IDs so that we make sure not to collide with
- // user-supplied ids on the same page.
- while (package.usedAnimationIdsByHref[href].contains(id)) {
- animationIdCount++;
- id = '$base$animationIdCount';
- }
- return id;
- }
-
- return rawDocs.replaceAllMapped(basicAnimationRegExp, (basicMatch) {
- var parser = ArgParser();
- parser.addOption('id');
- var args = _parseArgs(basicMatch[1], parser, 'animation');
- if (args == null) {
- // Already warned about an invalid parameter if this happens.
- return '';
- }
- final positionalArgs = args.rest.sublist(0);
- String uniqueId;
- var wasDeprecated = false;
- if (positionalArgs.length == 4) {
- // Supports the original form of the animation tag for backward
- // compatibility.
- uniqueId = positionalArgs.removeAt(0);
- wasDeprecated = true;
- } else if (positionalArgs.length == 3) {
- uniqueId = args['id'] ?? getUniqueId('animation_');
- } else {
- warn(PackageWarning.invalidParameter,
- message: 'Invalid @animation directive, "${basicMatch[0]}"\n'
- 'Animation directives must be of the form "{@animation WIDTH '
- 'HEIGHT URL [id=ID]}"');
- return '';
- }
-
- if (!validIdRegExp.hasMatch(uniqueId)) {
- warn(PackageWarning.invalidParameter,
- message: 'An animation has an invalid identifier, "$uniqueId". The '
- 'identifier can only contain letters, numbers and underscores, '
- 'and must not begin with a number.');
- return '';
- }
- if (package.usedAnimationIdsByHref[href].contains(uniqueId)) {
- warn(PackageWarning.invalidParameter,
- message: 'An animation has a non-unique identifier, "$uniqueId". '
- 'Animation identifiers must be unique.');
- return '';
- }
- package.usedAnimationIdsByHref[href].add(uniqueId);
-
- int width;
- try {
- width = int.parse(positionalArgs[0]);
- } on FormatException {
- warn(PackageWarning.invalidParameter,
- message: 'An animation has an invalid width ($uniqueId), '
- '"${positionalArgs[0]}". The width must be an integer.');
- return '';
- }
-
- int height;
- try {
- height = int.parse(positionalArgs[1]);
- } on FormatException {
- warn(PackageWarning.invalidParameter,
- message: 'An animation has an invalid height ($uniqueId), '
- '"${positionalArgs[1]}". The height must be an integer.');
- return '';
- }
-
- Uri movieUrl;
- try {
- movieUrl = Uri.parse(positionalArgs[2]);
- } on FormatException catch (e) {
- warn(PackageWarning.invalidParameter,
- message: 'An animation URL could not be parsed ($uniqueId): '
- '${positionalArgs[2]}\n$e');
- return '';
- }
- var overlayId = '${uniqueId}_play_button_';
-
- // Only warn about deprecation if some other warning didn't occur.
- if (wasDeprecated) {
- warn(PackageWarning.deprecated,
- message:
- 'Deprecated form of @animation directive, "${basicMatch[0]}"\n'
- 'Animation directives are now of the form "{@animation '
- 'WIDTH HEIGHT URL [id=ID]}" (id is an optional '
- 'parameter)');
- }
-
- return _modelElementRenderer.renderAnimation(
- uniqueId, width, height, movieUrl, overlayId);
- });
- }
-
/// Replace <[digest]> in API comments with
/// the contents of the HTML fragment earlier defined by the
/// {@inject-html} directive. The [digest] is a SHA1 of the contents
@@ -1639,133 +1232,8 @@ abstract class ModelElement extends Canonicalization
if (macro == null) {
warn(PackageWarning.unknownMacro, message: match[1]);
}
+ macro = processCommentDirectives(macro ?? '');
return macro;
});
}
-
- /// Parse and remove {@template ...} in API comments and store them
- /// in the index on the package.
- ///
- /// Syntax:
- ///
- /// {@template NAME}
- /// The contents of the macro
- /// {@endtemplate}
- ///
- String _stripMacroTemplatesAndAddToIndex(String rawDocs) {
- return rawDocs.replaceAllMapped(templateRegExp, (match) {
- packageGraph.addMacro(match[1].trim(), match[2].trim());
- return '{@macro ${match[1].trim()}}';
- });
- }
-
- /// Parse and remove {@inject-html ...} in API comments and store
- /// them in the index on the package, replacing them with a SHA1 hash of the
- /// contents, where the HTML will be re-injected after Markdown processing of
- /// the rest of the text is complete.
- ///
- /// Syntax:
- ///
- /// {@inject-html}
- /// The HTML to inject.
- /// {@end-inject-html}
- ///
- String _stripHtmlAndAddToIndex(String rawDocs) {
- if (!config.injectHtml) return rawDocs;
- return rawDocs.replaceAllMapped(htmlRegExp, (match) {
- var fragment = match[1];
- var digest = sha1.convert(fragment.codeUnits).toString();
- packageGraph.addHtmlFragment(digest, fragment);
- // The newlines are so that Markdown will pass this through without
- // touching it.
- return '\n$digest\n';
- });
- }
-
- /// Helper to process arguments given as a (possibly quoted) string.
- ///
- /// First, this will split the given [argsAsString] into separate arguments,
- /// taking any quoting (either ' or " are accepted) into account, including
- /// handling backslash-escaped quotes.
- ///
- /// Then, it will prepend "--" to any args that start with an identifier
- /// followed by an equals sign, allowing the argument parser to treat any
- /// "foo=bar" argument as "--foo=bar". It does handle quoted args like
- /// "foo='bar baz'" too, returning just bar (without quotes) for the foo
- /// value.
- Iterable _splitUpQuotedArgs(String argsAsString,
- {bool convertToArgs = false}) {
- final Iterable matches = argMatcher.allMatches(argsAsString);
- // Remove quotes around args, and if convertToArgs is true, then for any
- // args that look like assignments (start with valid option names followed
- // by an equals sign), add a "--" in front so that they parse as options.
- return matches.map((Match match) {
- var option = '';
- if (convertToArgs && match[1] != null && !match[1].startsWith('-')) {
- option = '--';
- }
- if (match[2] != null) {
- // This arg has quotes, so strip them.
- return '$option${match[1] ?? ''}${match[3] ?? ''}${match[4] ?? ''}';
- }
- return '$option${match[0]}';
- });
- }
-
- /// Helper to process arguments given as a (possibly quoted) string.
- ///
- /// First, this will split the given [argsAsString] into separate arguments
- /// with [_splitUpQuotedArgs] it then parses the resulting argument list
- /// normally with [argParser] and returns the result.
- ArgResults _parseArgs(
- String argsAsString, ArgParser argParser, String directiveName) {
- var args = _splitUpQuotedArgs(argsAsString, convertToArgs: true);
- try {
- return argParser.parse(args);
- } on ArgParserException catch (e) {
- warn(PackageWarning.invalidParameter,
- message: 'The {@$directiveName ...} directive was called with '
- 'invalid parameters. $e');
- return null;
- }
- }
-
- /// Helper for _injectExamples used to process @example arguments.
- /// Returns a map of arguments. The first unnamed argument will have key 'src'.
- /// The computed file path, constructed from 'src' and 'region' will have key
- /// 'file'.
- Map _getExampleArgs(String argsAsString) {
- var parser = ArgParser();
- parser.addOption('lang');
- parser.addOption('region');
- var results = _parseArgs(argsAsString, parser, 'example');
- if (results == null) {
- return null;
- }
-
- // Extract PATH and fix the path separators.
- var src = results.rest.isEmpty
- ? ''
- : results.rest.first.replaceAll('/', Platform.pathSeparator);
- var args = {
- 'src': src,
- 'lang': results['lang'],
- 'region': results['region'] ?? '',
- };
-
- // Compute 'file' from region and src.
- final fragExtension = '.md';
- var file = src + fragExtension;
- var region = args['region'] ?? '';
- if (region.isNotEmpty) {
- var dir = path.dirname(src);
- var basename = path.basenameWithoutExtension(src);
- var ext = path.extension(src);
- file = path.join(dir, '$basename-$region$ext$fragExtension');
- }
- args['file'] = config.examplePathPrefix == null
- ? file
- : path.join(config.examplePathPrefix, file);
- return args;
- }
}
diff --git a/lib/src/model/package_graph.dart b/lib/src/model/package_graph.dart
index 80b02d149f..a021b433f9 100644
--- a/lib/src/model/package_graph.dart
+++ b/lib/src/model/package_graph.dart
@@ -923,7 +923,11 @@ class PackageGraph {
}
void addHtmlFragment(String name, String content) {
- assert(!_localDocumentationBuilt);
+ // TODO(srawlins): We have to add HTML fragments after documentation is
+ // built, in order to include fragments which come from macros which
+ // were generated by a tool. I think the macro/HTML-injection system needs
+ // to be overhauled to allow for this type of looping.
+ //assert(!_localDocumentationBuilt);
_htmlFragments[name] = content;
}
}
diff --git a/test/model_special_cases_test.dart b/test/model_special_cases_test.dart
index 8f1666c06b..6ba938a707 100644
--- a/test/model_special_cases_test.dart
+++ b/test/model_special_cases_test.dart
@@ -266,6 +266,18 @@ void main() {
expect(injectHtmlFromTool.documentationAsHtml,
isNot(contains('{@end-inject-html}')));
});
+ test('tool outputs a macro which outputs injected HTML', () {
+ var ToolPrintingMacroWhichInjectsHtml = injectionExLibrary.allClasses
+ .firstWhere((c) => c.name == 'ToolPrintingMacroWhichInjectsHtml');
+ var a = ToolPrintingMacroWhichInjectsHtml.instanceFields
+ .firstWhere((m) => m.name == 'a');
+ expect(a.documentationAsHtml,
+ contains('Text.
\nTitle
'));
+ var b = ToolPrintingMacroWhichInjectsHtml.instanceFields
+ .firstWhere((m) => m.name == 'b');
+ expect(b.documentationAsHtml,
+ contains('Text.
\nTitle
'));
+ });
});
group('Missing and Remote', () {
diff --git a/testing/test_package/bin/print_macro.dart b/testing/test_package/bin/print_macro.dart
new file mode 100644
index 0000000000..9b4cd5c9d1
--- /dev/null
+++ b/testing/test_package/bin/print_macro.dart
@@ -0,0 +1,10 @@
+// Copyright (c) 2020, 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.
+
+// This is a sample "tool" used to test external tool integration into dartdoc.
+// It has no other purpose.
+
+void main() {
+ print('{@macro html-macro}');
+}
diff --git a/testing/test_package/dartdoc_options.yaml b/testing/test_package/dartdoc_options.yaml
index 305e927268..2e1d9d5321 100644
--- a/testing/test_package/dartdoc_options.yaml
+++ b/testing/test_package/dartdoc_options.yaml
@@ -11,6 +11,9 @@ dartdoc:
drill:
command: ["bin/drill.dart"]
description: "Puts holes in things."
+ print_macro:
+ command: ["bin/print_macro.dart"]
+ description: "Prints a macro."
echo:
macos: ['/bin/sh', '-c', 'echo']
linux: ['/bin/sh', '-c', 'echo']
diff --git a/testing/test_package/lib/example.dart b/testing/test_package/lib/example.dart
index bf656c76fe..d441643311 100644
--- a/testing/test_package/lib/example.dart
+++ b/testing/test_package/lib/example.dart
@@ -200,16 +200,14 @@ class Apple {
final ParameterizedTypedef fieldWithTypedef;
}
-
/// Extension on Apple
extension AppleExtension on Apple {
-/// Can call s on Apple
+ /// Can call s on Apple
void s() {
print('Extension on Apple');
}
}
-
class WithGeneric {
T prop;
WithGeneric(this.prop);
@@ -666,7 +664,7 @@ class ExtensionUser {
/// Extension on List
extension FancyList on List {
int get doubleLength => this.length * 2;
- List operator-() => this.reversed.toList();
+ List operator -() => this.reversed.toList();
List> split(int at) =>
>[this.sublist(0, at), this.sublist(at)];
static List big() => List(1000000);
@@ -674,7 +672,7 @@ extension FancyList on List {
extension SymDiff on Set {
Set symmetricDifference(Set other) =>
- this.difference(other).union(other.difference(this));
+ this.difference(other).union(other.difference(this));
}
/// Extensions can be made specific.
@@ -684,16 +682,30 @@ extension IntSet on Set {
// Extensions can be private.
extension _Shhh on Object {
- void secret() { }
+ void secret() {}
}
// Extension with no name
extension on Object {
- void bar() { }
+ void bar() {}
}
-
/// This class has nothing to do with [_Shhh], [FancyList], or [AnExtension.call],
/// but should not crash because we referenced them.
/// We should be able to find [DocumentThisExtensionOnce], too.
class ExtensionReferencer {}
+
+class ToolPrintingMacroWhichInjectsHtml {
+ /// Text.
+ /// {@template html-macro}
+ /// {@inject-html}Title
{@end-inject-html}
+ /// {@endtemplate}
+ int a;
+
+ /// Text.
+ ///
+ /// {@tool print_macro}
+ /// Text for tool.
+ /// {@end-tool}
+ int b;
+}
From 88da0750a79cfe3280654d593d04d1489fdb59f1 Mon Sep 17 00:00:00 2001
From: Sam Rawlins
Date: Fri, 24 Jul 2020 07:26:11 -0700
Subject: [PATCH 2/3] test
---
test/model_test.dart | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/model_test.dart b/test/model_test.dart
index 7352b37d12..90baabe686 100644
--- a/test/model_test.dart
+++ b/test/model_test.dart
@@ -1617,7 +1617,7 @@ void main() {
});
test('correctly finds all the classes', () {
- expect(classes, hasLength(33));
+ expect(classes, hasLength(34));
});
test('abstract', () {
From 95d022591bc6b5769b912e50e97fdb1809a0197e Mon Sep 17 00:00:00 2001
From: Sam Rawlins
Date: Fri, 24 Jul 2020 14:17:00 -0700
Subject: [PATCH 3/3] Comment out one more assert.
---
lib/src/model/package_graph.dart | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/lib/src/model/package_graph.dart b/lib/src/model/package_graph.dart
index a021b433f9..9f235c8ea9 100644
--- a/lib/src/model/package_graph.dart
+++ b/lib/src/model/package_graph.dart
@@ -913,7 +913,11 @@ class PackageGraph {
}
void addMacro(String name, String content) {
- assert(!_localDocumentationBuilt);
+ // TODO(srawlins): We have to add HTML fragments after documentation is
+ // built, in order to include fragments which come from macros which
+ // were generated by a tool. I think the macro/HTML-injection system needs
+ // to be overhauled to allow for this type of looping.
+ //assert(!_localDocumentationBuilt);
_macros[name] = content;
}