blob: 2914cbc32633639d0d1ed483bdae4f94f3871b47 [file] [log] [blame]
// Copyright (c) 2023, 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.
/// Various classes for data computed for doc comments.
library;
import 'package:analyzer/dart/ast/ast.dart';
import 'package:meta/meta.dart';
/// A block doc directive, denoted by an opening tag, and a closing tag.
///
/// The text in between the two tags is not explicitly called out. It can be
/// read from the original compilation unit, between the offsets of the opening
/// and closing tags.
@experimental
final class BlockDocDirective implements DocDirective {
final DocDirectiveTag openingTag;
final DocDirectiveTag? closingTag;
BlockDocDirective(this.openingTag, this.closingTag);
@override
DocDirectiveType get type => openingTag.type;
}
/// The type of Markdown code block in a documentation comment.
@experimental
enum CodeBlockType {
/// Fenced code blocks begin with a code fence, preceded by up to three spaces
/// of indentation.
///
/// A code fence is a sequence of at least three consecutive backtick
/// characters (`) or tildes (~).
fenced,
/// Indented code blocks have every line of code preceded by four or more
/// spaces of indentation.
indented,
}
/// An instance of a [DocDirectiveType] in the text of a doc comment, either
/// as a [SimpleDocDirective], represented by a single [DocDirectiveTag], or a
/// [BlockDocDirective], represented by an opening [DocDirectiveTag] and a
/// closing one (in well-formed text).
@experimental
sealed class DocDirective {
DocDirectiveType get type;
}
/// An argument in a doc directive. See [DocDirective] for their syntax.
@experimental
sealed class DocDirectiveArgument {
/// The offset of the start of the argument, from the beginning of the
/// compilation unit.
final int offset;
/// The offset just after the end of the argument, from the beginning of the
/// compilation unit.
final int end;
/// The value of the argument.
final String value;
DocDirectiveArgument({
required this.offset,
required this.end,
required this.value,
});
}
/// A named argument in a doc directive. See [DocDirective] for their syntax.
@experimental
final class DocDirectiveNamedArgument extends DocDirectiveArgument {
/// The name of the argument.
final String name;
DocDirectiveNamedArgument({
required super.offset,
required super.end,
required this.name,
required super.value,
});
}
/// A parameter in a doc directive, with it's expected format, if it has one.
@experimental
final class DocDirectiveParameter {
final String name;
final DocDirectiveParameterFormat expectedFormat;
const DocDirectiveParameter(this.name, this.expectedFormat);
}
/// The expected format of a doc directive parameter, which indicates some
/// minimal validation that can produce diagnostics.
@experimental
enum DocDirectiveParameterFormat {
/// A format indicating that arguments are not validated.
any('any'),
/// A format indicating that an argument must be parsable as an integer.
integer('an integer'),
/// A format indicating that an argument must be parsable as a URI.
uri('a URI'),
/// A format indicating that an argument must be parsable as a URI, and be in
/// the format of a YouTube video URL.
youtubeUrl("a YouTube URL, starting with '$youtubeUrlPrefix'");
static const youtubeUrlPrefix = 'https://www.youtube.com/watch?v=';
final String displayString;
const DocDirectiveParameterFormat(this.displayString);
}
/// A positional argument in a doc directive. See [DocDirective] for their
/// syntax.
@experimental
final class DocDirectivePositionalArgument extends DocDirectiveArgument {
DocDirectivePositionalArgument({
required super.offset,
required super.end,
required super.value,
});
}
/// A documentation directive, found in a doc comment.
///
/// Documentation directives are declared with `{@` at the start of a line of a
/// documentation comment, followed the name of a doc directive, arguments, and
/// finally a right curly brace (`}`).
///
/// Arguments are separated from the directive name, and from each other, by
/// whitespace. There are two types of arguments: positional and named. Named
/// arguments are written as `NAME=VALUE`, without any internal whitespace.
/// Named arguments can be optional.
@experimental
final class DocDirectiveTag {
/// The offset of the starting text; for example: '@animation'.
final int offset;
final int end;
final int nameOffset;
final int nameEnd;
final DocDirectiveType type;
final List<DocDirectiveArgument> positionalArguments;
final List<DocDirectiveNamedArgument> namedArguments;
DocDirectiveTag({
required this.offset,
required this.end,
required this.nameOffset,
required this.nameEnd,
required this.type,
required this.positionalArguments,
required this.namedArguments,
});
}
@experimental
enum DocDirectiveType {
/// A [DocDirective] declaring an embedded video with HTML video controls.
///
/// This directive has three required arguments: the width, the height, and
/// the URL. A named 'id' argument can also be given. For example:
///
/// ```none
/// {@animation 600 400 https://www.example.com/example.mp4 id=video1}
/// ```
///
/// See documentation at
/// <https://github.com/dart-lang/dartdoc/blob/main/doc/directives.md#animation---animations>.
animation(
'animation',
positionalParameters: [
DocDirectiveParameter('width', DocDirectiveParameterFormat.integer),
DocDirectiveParameter('height', DocDirectiveParameterFormat.integer),
DocDirectiveParameter('url', DocDirectiveParameterFormat.uri),
],
namedParameters: [
DocDirectiveParameter('id', DocDirectiveParameterFormat.any),
],
),
/// A [DocDirective] declaring the associated library is the "canonical"
/// location for a certain element.
///
/// Dartdoc uses some heuristics to decide what the public-facing libraries
/// are, and which public-facing library is the "canonical" location for an
/// element. When that heuristic needs to be overridden, a user can use this
/// directive. Example:
///
/// ```none
/// {@canonicalFor some_library.SomeClass}
/// ```
///
/// See documentation at
/// <https://github.com/dart-lang/dartdoc/blob/main/doc/directives.md#canonicalfor---canonicalization>.
canonicalFor(
// TODO(srawlins): We have mostly used 'kebab-case' in directive names. This
// directive name is a rare departure from that style. Migrate users to use
// 'canonical-for'.
'canonicalFor',
positionalParameters: [
DocDirectiveParameter('element', DocDirectiveParameterFormat.any),
],
),
/// A [DocDirective] declaring a categorization into a named category.
///
/// This directive has one required argument: the category name. The category
/// name is allowed to contain whitespace.
// TODO(srawlins): I think allowing a category name, which is parsed as
// multiple positional arguments (or I guess named arguments if one contains)
// an equal sign!) is too loosy-goosey. We should just support quoting and
// require that a category name with spaces be wrapped in quotes.
///
/// See documentation at
/// <https://github.com/dart-lang/dartdoc/blob/main/doc/directives.md#category-and-subcategory---categories>.
category('category', restParametersAllowed: true),
/// The end tag for the [DocDirectiveType.injectHtml] tag.
///
/// This tag should not really constitute a "type" of doc directive, but this
/// implementation is a one-to-one mapping of "types" and "tags", so end tags
/// are included. This also allows us to parse (erroneous) dangling end tags.
endInjectHtml.end('end-inject-html', openingTag: 'inject-html'),
/// The end tag for the [DocDirectiveType.tool] tag.
///
/// This tag should not really constitute a "type" of doc directive, but this
/// implementation is a one-to-one mapping of "types" and "tags", so end tags
/// are included. This also allows us to parse (erroneous) dangling end tags.
endTool.end('end-tool', openingTag: 'tool'),
/// The end tag for the [DocDirectiveType.template] tag.
///
/// This tag should not really constitute a "type" of doc directive, but this
/// implementation is a one-to-one mapping of "types" and "tags", so end tags
/// are included. This also allows us to parse (erroneous) dangling end tags.
endTemplate.end('endtemplate', openingTag: 'template'),
/// A [DocDirective] declaring a block of HTML content which is to be inserted
/// after all other processing, including Markdown parsing.
///
/// See documentation at
/// <https://github.com/dart-lang/dartdoc/blob/main/doc/directives.md#inject-html---injected-html>.
injectHtml.block('inject-html', 'end-inject-html'),
/// A [DocDirective] declaring a macro application.
///
/// This directive has one required argument: the name. For example:
///
/// ```none
/// {@macro some-macro}
/// ```
macro(
'macro',
positionalParameters: [
DocDirectiveParameter('name', DocDirectiveParameterFormat.any),
],
),
/// A [DocDirective] declaring a categorization into a named sub-category.
///
/// This directive has one required argument: the sub-category name. The
/// sub-category name is allowed to contain whitespace.
///
/// See documentation at
/// <https://github.com/dart-lang/dartdoc/blob/main/doc/directives.md#category-and-subcategory---categories>.
subCategory(
// TODO(srawlins): We have mostly used 'kebab-case' in directive names. This
// directive name is the sole departure from that style. Migrate users to
// use 'sub-category'.
'subCategory',
restParametersAllowed: true,
),
/// A [DocDirective] declaring a template of text which can be applied to
/// other doc comments with a macro.
///
/// A template can contain any recognized doc comment content between the
/// opening and closing tags, like Markdown text, comment references, and
/// simple doc directives.
///
/// See documentation at
/// <https://github.com/dart-lang/dartdoc/blob/main/doc/directives.md#template-and-macro---templates-and-macros>.
// TODO(srawlins): Migrate users to use 'end-template'.
template.block(
'template',
'endtemplate',
positionalParameters: [
DocDirectiveParameter('name', DocDirectiveParameterFormat.any),
],
),
/// A [DocDirective] declaring a tool.
///
/// A tool directive invokes an external tool, with the text between the
/// opening and closing tags as stdin, and replaces the directive with the
/// output of the tool.
///
/// See documentation at
/// <https://github.com/dart-lang/dartdoc/blob/main/doc/directives.md#tool---external-tools>.
tool.block(
'tool',
'end-tool',
positionalParameters: [
DocDirectiveParameter('name', DocDirectiveParameterFormat.any),
],
restParametersAllowed: true,
),
/// A [DocDirective] declaring an embedded YouTube video.
///
/// This directive has three required arguments: the width, the height, and
/// the URL. For example:
///
/// ```
/// {@youtube 600 400 https://www.youtube.com/watch?v=abc123}
/// ```
///
/// See documentation at
/// <https://github.com/dart-lang/dartdoc/blob/main/doc/directives.md#youtube---youtube-videos>.
youtube(
'youtube',
positionalParameters: [
DocDirectiveParameter('width', DocDirectiveParameterFormat.integer),
DocDirectiveParameter('height', DocDirectiveParameterFormat.integer),
DocDirectiveParameter('url', DocDirectiveParameterFormat.youtubeUrl),
],
);
/// Whether this starts a block directive, which must be closed by a specific
/// closing directive.
///
/// For example, the 'inject-html' directive begins with `{@inject-html}` and
/// ends with `{@end-inject-html}`.
final bool isBlock;
/// The name of the directive, as written in a doc comment.
final String name;
/// The name of the directive that ends this one, in the case of a block
/// directive's opening tag, the name of the directive that starts this one,
/// in the case of a block directive's closing tag, and `null` otherwise.
final String? opposingName;
/// The positional parameters, which are each required.
final List<DocDirectiveParameter> positionalParameters;
/// The named parameters, which are each optional.
final List<DocDirectiveParameter> namedParameters;
/// Whether "rest" parameters are allowed.
///
/// In such a doc directive type, we do not enforce a maximum number of
/// arguments.
final bool restParametersAllowed;
const DocDirectiveType(
this.name, {
this.positionalParameters = const <DocDirectiveParameter>[],
this.namedParameters = const <DocDirectiveParameter>[],
this.restParametersAllowed = false,
}) : isBlock = false,
opposingName = null;
const DocDirectiveType.block(
this.name,
this.opposingName, {
this.positionalParameters = const <DocDirectiveParameter>[],
this.restParametersAllowed = false,
}) : isBlock = true,
namedParameters = const <DocDirectiveParameter>[];
const DocDirectiveType.end(this.name, {required String openingTag})
: opposingName = openingTag,
isBlock = false,
positionalParameters = const <DocDirectiveParameter>[],
namedParameters = const <DocDirectiveParameter>[],
restParametersAllowed = false;
}
/// A documentation import, found in a doc comment.
///
/// Documentation imports are declared with `@docImport` at the start of a line
/// of a documentation comment, followed by regular import elements (URI,
/// optional prefix, optional combinators), ending with a semicolon.
@experimental
final class DocImport {
/// The offset of the starting text, '@docImport'.
int offset;
ImportDirective import;
DocImport({required this.offset, required this.import});
}
/// A Markdown fenced code block found in a documentation comment.
@experimental
final class MdCodeBlock {
/// The 'info string'.
///
/// This includes any text (trimming whitespace) following the opening
/// backticks (for a fenced code block). For example, in a fenced code block
/// starting with "```dart", the info string is "dart".
///
/// If the code block is an indented code block, or a fenced code block with
/// no text following the opening backticks, the info string is `null`.
///
/// See CommonMark specification at
/// <https://spec.commonmark.org/0.30/#fenced-code-blocks>.
final String? infoString;
/// Information about the comment lines that make up this code block.
///
/// For a fenced code block, these lines include the opening and closing
/// fence delimiter lines.
final List<MdCodeBlockLine> lines;
/// The type of Markdown code block that is being represented.
final CodeBlockType type;
MdCodeBlock({
required this.infoString,
required List<MdCodeBlockLine> lines,
required this.type,
}) : lines = List.of(lines, growable: false);
}
/// A Markdown code block line found in a documentation comment.
@experimental
final class MdCodeBlockLine {
/// The offset of the start of the code block, from the beginning of the
/// compilation unit.
final int offset;
/// The length of the fenced code block.
final int length;
MdCodeBlockLine({required this.offset, required this.length});
}
@experimental
final class SimpleDocDirective implements DocDirective {
final DocDirectiveTag tag;
SimpleDocDirective(this.tag);
@override
DocDirectiveType get type => tag.type;
}