blob: ca288d9fd3a831ba6d32bd5bf09922b06a89375b [file] [log] [blame]
// Copyright (c) 2015, 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.
/// Utility code to convert markdown comments to html.
library dartdoc.markdown_processor;
import 'dart:convert';
import 'dart:math';
import 'package:analyzer/dart/element/element.dart';
import 'package:dartdoc/src/element_type.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/tuple.dart';
import 'package:dartdoc/src/warnings.dart';
import 'package:markdown/markdown.dart' as md;
const validHtmlTags = [
'a',
'abbr',
'address',
'area',
'article',
'aside',
'audio',
'b',
'bdi',
'bdo',
'blockquote',
'br',
'button',
'canvas',
'caption',
'cite',
'code',
'col',
'colgroup',
'data',
'datalist',
'dd',
'del',
'dfn',
'div',
'dl',
'dt',
'em',
'fieldset',
'figcaption',
'figure',
'footer',
'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'header',
'hr',
'i',
'iframe',
'img',
'input',
'ins',
'kbd',
'keygen',
'label',
'legend',
'li',
'link',
'main',
'map',
'mark',
'meta',
'meter',
'nav',
'noscript',
'object',
'ol',
'optgroup',
'option',
'output',
'p',
'param',
'pre',
'progress',
'q',
's',
'samp',
'script',
'section',
'select',
'small',
'source',
'span',
'strong',
'style',
'sub',
'sup',
'table',
'tbody',
'td',
'template',
'textarea',
'tfoot',
'th',
'thead',
'time',
'title',
'tr',
'track',
'u',
'ul',
'var',
'video',
'wbr'
];
final RegExp nonHTML =
RegExp("</?(?!(${validHtmlTags.join("|")})[> ])\\w+[> ]");
/// Things to ignore at the end of a doc reference.
///
/// This is intended to catch type parameters/arguments, and function call
/// parentheses.
final _trailingIgnorePattern = RegExp(r'(<.*>|\(.*\)|\?|!)$');
@Deprecated('Public variable intended to be private; will be removed as early '
'as Dartdoc 1.0.0')
RegExp get trailingIgnoreStuff => _trailingIgnorePattern;
/// Things to ignore at the beginning of a doc reference.
///
/// This is intended to catch various keywords that a developer may include in
/// front of a variable name.
// TODO(srawlins): I cannot find any tests asserting we can resolve such
// references.
final _leadingIgnorePattern =
RegExp(r'^(const|final|var)[\s]+', multiLine: true);
@Deprecated('Public variable intended to be private; will be removed as early '
'as Dartdoc 1.0.0')
RegExp get leadingIgnoreStuff => _leadingIgnorePattern;
/// If found, this pattern may indicate a reference to a constructor.
final _constructorIndicationPattern =
RegExp(r'(^new[\s]+|\(\)$)', multiLine: true);
@Deprecated('Public variable intended to be private; will be removed as early '
'as Dartdoc 1.0.0')
RegExp get isConstructor => _constructorIndicationPattern;
/// A pattern indicating that text which looks like a doc reference is not
/// intended to be.
///
/// This covers anything with leading digits/symbols, empty strings, weird
/// punctuation, spaces.
///
/// The idea is to catch such cases and not produce warnings about the contents.
final RegExp notARealDocReference = RegExp(r'''(^[^\w]|^[\d]|[,"'/]|^$)''');
final RegExp operatorPrefix = RegExp(r'^operator[ ]*');
final HtmlEscape htmlEscape = const HtmlEscape(HtmlEscapeMode.element);
final List<md.InlineSyntax> _markdown_syntaxes = [
_InlineCodeSyntax(),
_AutolinkWithoutScheme(),
md.InlineHtmlSyntax(),
md.StrikethroughSyntax(),
md.AutolinkExtensionSyntax(),
];
final List<md.BlockSyntax> _markdown_block_syntaxes = [
const md.FencedCodeBlockSyntax(),
const md.HeaderWithIdSyntax(),
const md.SetextHeaderWithIdSyntax(),
const md.TableSyntax(),
];
// Remove these schemas from the display text for hyperlinks.
final RegExp _hide_schemes = RegExp('^(http|https)://');
class MatchingLinkResult {
final ModelElement element;
final bool warn;
MatchingLinkResult(this.element, {this.warn = true});
}
class IterableBlockParser extends md.BlockParser {
IterableBlockParser(List<String> lines, md.Document document)
: super(lines, document);
Iterable<md.Node> parseLinesGenerator() sync* {
while (!isDone) {
for (var syntax in blockSyntaxes) {
if (syntax.canParse(this)) {
var block = syntax.parse(this);
if (block != null) yield (block);
break;
}
}
}
}
}
// Calculate a class hint for findCanonicalModelElementFor.
ModelElement _getPreferredClass(ModelElement modelElement) {
if (modelElement is EnclosedElement &&
(modelElement as EnclosedElement).enclosingElement is Container) {
return (modelElement as EnclosedElement).enclosingElement;
} else if (modelElement is Class) {
return modelElement;
}
return null;
}
/// Returns null if element is a parameter.
MatchingLinkResult _getMatchingLinkElement(
String codeRef, Warnable element, List<ModelCommentReference> commentRefs) {
if (!codeRef.contains(_constructorIndicationPattern) &&
codeRef.contains(notARealDocReference)) {
// Don't waste our time on things we won't ever find.
return MatchingLinkResult(null, warn: false);
}
ModelElement refModelElement;
// Try expensive not-scoped lookup.
if (refModelElement == null && element is ModelElement) {
Container preferredClass = _getPreferredClass(element);
if (preferredClass is Extension) {
element.warn(PackageWarning.notImplemented,
message:
'Comment reference resolution inside extension methods is not yet implemented');
} else {
refModelElement = _MarkdownCommentReference(
codeRef, element, commentRefs, preferredClass)
.computeReferredElement();
}
}
// Did not find it anywhere.
if (refModelElement == null) {
// TODO(jcollins-g): remove squelching of non-canonical warnings here
// once we no longer process full markdown for
// oneLineDocs (#1417)
return MatchingLinkResult(null, warn: element.isCanonical);
}
// Ignore all parameters.
if (refModelElement is Parameter || refModelElement is TypeParameter) {
return MatchingLinkResult(null, warn: false);
}
// There have been places in the code which helpfully cache entities
// regardless of what package they are associated with. This assert
// will protect us from reintroducing that.
assert(refModelElement == null ||
refModelElement.packageGraph == element.packageGraph);
if (refModelElement != null) {
return MatchingLinkResult(refModelElement);
}
// From this point on, we haven't been able to find a canonical ModelElement.
if (!refModelElement.isCanonical) {
if (refModelElement.library.isPublicAndPackageDocumented) {
refModelElement
.warn(PackageWarning.noCanonicalFound, referredFrom: [element]);
}
// Don't warn about doc references because that's covered by the no
// canonical library found message.
return MatchingLinkResult(null, warn: false);
}
// We should never get here unless there's a bug in findCanonicalModelElementFor.
// findCanonicalModelElementFor(searchElement, preferredClass: preferredClass)
// should only return null if ModelElement.from(searchElement, refLibrary)
// would return a non-canonical element. However, outside of checked mode,
// at least we have a canonical element, so proceed.
assert(false);
return MatchingLinkResult(refModelElement);
}
/// Given a set of commentRefs, return the one whose name matches the codeRef.
Element _getRefElementFromCommentRefs(
List<ModelCommentReference> commentRefs, String codeRef) {
if (commentRefs != null) {
for (var ref in commentRefs) {
if (ref.name == codeRef) {
var isConstrElement = ref.staticElement is ConstructorElement;
// Constructors are now handled by library search.
if (!isConstrElement) {
var refElement = ref.staticElement;
if (refElement is PropertyAccessorElement) {
// yay we found an accessor that wraps a const, but we really
// want the top-level field itself
refElement = (refElement as PropertyAccessorElement).variable;
}
if (refElement is PrefixElement) {
// We found a prefix element, but what we really want is the library element.
refElement = (refElement as PrefixElement).enclosingElement;
}
return refElement;
}
}
}
}
return null;
}
/// Represents a single comment reference.
class _MarkdownCommentReference {
/// The code reference text.
final String codeRef;
/// The element containing the code reference.
final Warnable element;
/// A list of [ModelCommentReference]s for this element.
final List<ModelCommentReference> commentRefs;
/// Disambiguate inheritance with this class.
final Class preferredClass;
/// Current results. Input/output of all _find and _reduce methods.
Set<ModelElement> results;
/// codeRef with any leading constructor string, stripped.
String codeRefChomped;
/// Library associated with this element.
Library library;
/// PackageGraph associated with this element.
PackageGraph packageGraph;
_MarkdownCommentReference(
this.codeRef, this.element, this.commentRefs, this.preferredClass) {
assert(element != null);
assert(element.packageGraph.allLibrariesAdded);
codeRefChomped = codeRef.replaceAll(_constructorIndicationPattern, '');
library =
element is ModelElement ? (element as ModelElement).library : null;
packageGraph = library.packageGraph;
}
String __impliedUnnamedConstructor;
/// [_impliedUnnamedConstructor] is memoized in [__impliedUnnamedConstructor],
/// but even after it is initialized, it may be null. This bool represents the
/// initializiation state.
bool __impliedUnnamedConstructorIsSet = false;
/// Returns the name of the implied unnamed constructor if there is one, or
/// null if not.
///
/// Unnamed constructors are a special case in dartdoc. If we look up a name
/// within a class of that class itself, the first thing we find is the
/// unnamed constructor. But we determine whether that's what they actually
/// intended (vs. the enclosing class) by context -- whether they seem
/// to be calling it with () or have a 'new' in front of it, or
/// whether the name is repeated.
///
/// Similarly, referencing a class by itself might actually refer to its
/// unnamed constructor based on these same heuristics.
///
/// With the name of the implied unnamed constructor, other methods can
/// determine whether or not the constructor and/or class we resolved to
/// is actually matching the user's intent.
String get _impliedUnnamedConstructor {
if (!__impliedUnnamedConstructorIsSet) {
__impliedUnnamedConstructorIsSet = true;
if (codeRef.contains(_constructorIndicationPattern) ||
(codeRefChompedParts.length >= 2 &&
codeRefChompedParts[codeRefChompedParts.length - 1] ==
codeRefChompedParts[codeRefChompedParts.length - 2])) {
// If the last two parts of the code reference are equal, this is probably a default constructor.
__impliedUnnamedConstructor = codeRefChompedParts.last;
}
}
return __impliedUnnamedConstructor;
}
/// Calculate reference to a ModelElement.
///
/// Uses a series of calls to the _find* methods in this class to get one
/// or more possible [ModelElement] matches, then uses the _reduce* methods
/// in this class to try to bring it to a single ModelElement. Calls
/// [element.warn] for [PackageWarning.ambiguousDocReference] if there
/// are more than one, but does not warn otherwise.
ModelElement computeReferredElement() {
results = {};
// TODO(jcollins-g): A complex package winds up spending a lot of cycles in
// here. Optimize.
for (var findMethod in [
// This might be an operator. Strip the operator prefix and try again.
_findWithoutOperatorPrefix,
// Oh, and someone might have thrown on a 'const' or 'final' in front.
_findWithoutLeadingIgnoreStuff,
// Maybe this ModelElement has parameters, and this is one of them.
// We don't link these, but this keeps us from emitting warnings. Be sure
// to get members of parameters too.
_findParameters,
// Maybe this ModelElement has type parameters, and this is one of them.
_findTypeParameters,
// This could be local to the class, look there first.
_findWithinTryClasses,
// This could be a reference to a renamed library.
_findReferenceFromPrefixes,
// We now need the ref element cache to keep from repeatedly searching
// [Package.allModelElements]. But if not, look for a fully qualified
// match. (That only makes sense if the code reference might be
// qualified, and contains periods.)
_findWithinRefElementCache,
// Only look for partially qualified matches if we didn't find a fully
// qualified one.
_findPartiallyQualifiedMatches,
// Only look for partially qualified matches if we didn't find a fully
// qualified one.
_findGlobalWithinRefElementCache,
// This could conceivably be a reference to an enum member. They don't
// show up in [allModelElements].
_findEnumReferences,
// Oh, and someone might have some type parameters or other garbage.
// After finding within classes because sometimes parentheses are used to
// imply constructors.
_findWithoutTrailingIgnoreStuff,
// Use the analyzer to resolve a comment reference.
_findAnalyzerReferences,
]) {
findMethod();
// Remove any "null" objects after each step of trying to add to results.
// TODO(jcollins-g): Eliminate all situations where nulls can be added
// to the results set.
results.remove(null);
if (results.isNotEmpty) break;
}
if (results.length > 1) {
// This isn't C++. References to class methods are slightly expensive
// in Dart so don't build that list unless you need to.
for (var reduceMethod in [
// If a result is actually in this library, prefer that.
_reducePreferResultsInSameLibrary,
// If a result is accessible in this library, prefer that.
_reducePreferResultsAccessibleInSameLibrary,
// This may refer to an element with the same name in multiple libraries
// in an external package, e.g. Matrix4 in vector_math and
// vector_math_64. Disambiguate by attempting to figure out which of
// them our package is actually using by checking the import/export
// graph.
_reducePreferLibrariesInLocalImportExportGraph,
// If a result's fully qualified name has pieces of the comment
// reference, prefer that.
_reducePreferReferencesIncludingFullyQualifiedName,
// If the reference is indicated to be a constructor, prefer
// constructors. This is not as generic as it sounds; very few naming
// conflicts are allowed, but an instance field is allowed to have the
// same name as a named constructor.
_reducePreferConstructorViaIndicators,
// Prefer Fields/TopLevelVariables to accessors.
// TODO(jcollins-g): Remove after fixing dart-lang/dartdoc#2396 or
// exclude Accessors from all lookup tables.
_reducePreferCombos,
// Prefer the Dart analyzer's resolution of comment references. We
// can't start from this because of the differences in Dartdoc
// canonicalization.
_reducePreferAnalyzerResolution,
]) {
reduceMethod();
if (results.length <= 1) break;
}
}
ModelElement result;
// TODO(jcollins-g): further disambiguations based on package information?
if (results.isEmpty) {
result = null;
} else if (results.length == 1) {
result = results.first;
} else {
// Squelch ambiguous doc reference warnings for parameters, because we
// don't link those anyway.
if (!results.every((r) => r is Parameter)) {
var elementNames = results.map((r) => "'${r.fullyQualifiedName}'");
element.warn(PackageWarning.ambiguousDocReference,
message: '[$codeRef] => ${elementNames.join(', ')}');
}
result = results.first;
}
return result;
}
List<String> _codeRefParts;
List<String> get codeRefParts => _codeRefParts ??= codeRef.split('.');
List<String> _codeRefChompedParts;
List<String> get codeRefChompedParts =>
_codeRefChompedParts ??= codeRefChomped.split('.');
void _reducePreferAnalyzerResolution() {
var refElement = _getRefElementFromCommentRefs(commentRefs, codeRef);
if (results.any((me) => me.element == refElement)) {
results.removeWhere((me) => me.element != refElement);
}
}
void _reducePreferConstructorViaIndicators() {
if (codeRef.contains(_constructorIndicationPattern) &&
codeRefChompedParts.length >= 2) {
results.removeWhere((r) => r is! Constructor);
}
}
void _reducePreferReferencesIncludingFullyQualifiedName() {
var startName = '${element.fullyQualifiedName}.';
var realName = '${element.fullyQualifiedName}.${codeRefChomped}';
if (results.any((r) => r.fullyQualifiedName == realName)) {
results.removeWhere((r) => r.fullyQualifiedName != realName);
}
if (results.any((r) => r.fullyQualifiedName.startsWith(startName))) {
results.removeWhere((r) => !r.fullyQualifiedName.startsWith(startName));
}
}
void _reducePreferLibrariesInLocalImportExportGraph() {
if (results.any(
(r) => library.packageImportedExportedLibraries.contains(r.library))) {
results.removeWhere(
(r) => !library.packageImportedExportedLibraries.contains(r.library));
}
}
void _reducePreferResultsAccessibleInSameLibrary() {
// TODO(jcollins-g): we could have saved ourselves some work by using the analyzer
// to search the namespace, somehow. Do that instead.
if (element is ModelElement &&
results.any((r) => r.element
.isAccessibleIn((element as ModelElement).library.element))) {
results.removeWhere((r) =>
!r.element.isAccessibleIn((element as ModelElement).library.element));
}
}
void _reducePreferResultsInSameLibrary() {
if (results.any((r) => r.library?.packageName == library.packageName)) {
results.removeWhere((r) => r.library?.packageName != library.packageName);
}
}
void _reducePreferCombos() {
var accessors = results.whereType<Accessor>().toList();
accessors.forEach((a) {
results.remove(a);
results.add(a.enclosingCombo);
});
}
void _findTypeParameters() {
if (element is TypeParameters) {
results.addAll((element as TypeParameters).typeParameters.where((p) =>
p.name == codeRefChomped || codeRefChomped.startsWith('${p.name}.')));
}
}
void _findParameters() {
if (element is ModelElement) {
results.addAll((element as ModelElement).allParameters.where((p) =>
p.name == codeRefChomped || codeRefChomped.startsWith('${p.name}.')));
}
}
void _findWithoutLeadingIgnoreStuff() {
if (codeRef.contains(_leadingIgnorePattern)) {
var newCodeRef = codeRef.replaceFirst(_leadingIgnorePattern, '');
results.add(_MarkdownCommentReference(
newCodeRef, element, commentRefs, preferredClass)
.computeReferredElement());
}
}
void _findWithoutTrailingIgnoreStuff() {
if (codeRef.contains(_trailingIgnorePattern)) {
var newCodeRef = codeRef.replaceFirst(_trailingIgnorePattern, '');
results.add(_MarkdownCommentReference(
newCodeRef, element, commentRefs, preferredClass)
.computeReferredElement());
}
}
void _findWithoutOperatorPrefix() {
if (codeRef.startsWith(operatorPrefix)) {
var newCodeRef = codeRef.replaceFirst(operatorPrefix, '');
results.add(_MarkdownCommentReference(
newCodeRef, element, commentRefs, preferredClass)
.computeReferredElement());
}
}
void _findEnumReferences() {
// TODO(jcollins-g): Put enum members in allModelElements with useful hrefs without blowing up other assumptions about what that means.
// TODO(jcollins-g): This doesn't provide good warnings if an enum and class have the same name in different libraries in the same package. Fix that.
if (codeRefChompedParts.length >= 2) {
var maybeEnumName = codeRefChompedParts
.sublist(0, codeRefChompedParts.length - 1)
.join('.');
var maybeEnumMember = codeRefChompedParts.last;
if (packageGraph.findRefElementCache.containsKey(maybeEnumName)) {
for (final modelElement
in packageGraph.findRefElementCache[maybeEnumName]) {
if (modelElement is Enum) {
if (modelElement.constantFields
.any((e) => e.name == maybeEnumMember)) {
results.add(modelElement);
break;
}
}
}
}
}
}
/// Returns the unnamed constructor for class [toConvert] or the class for
/// constructor [toConvert], or just [toConvert], based on hueristics.
///
/// * If an unnamed constructor is implied in the comment reference, and
/// [toConvert] is a class with the same name, the class's unnamed
/// constructor is returned.
/// * Otherwise, if [toConvert] is an unnamed constructor, its enclosing
/// class is returned.
/// * Othwerwise, [toConvert] is returned.
ModelElement _convertConstructors(ModelElement toConvert) {
if (_impliedUnnamedConstructor != null) {
if (toConvert is Class && toConvert.name == _impliedUnnamedConstructor) {
return toConvert.unnamedConstructor;
}
return toConvert;
} else {
if (toConvert is Constructor &&
(toConvert.enclosingElement as Class).unnamedConstructor ==
toConvert) {
return toConvert.enclosingElement;
}
return toConvert;
}
}
void _findReferenceFromPrefixes() {
if (element is! ModelElement) return;
var prefixToLibrary =
(element as ModelElement).definingLibrary.prefixToLibrary;
if (prefixToLibrary.containsKey(codeRefChompedParts.first)) {
if (codeRefChompedParts.length == 1) {
results.addAll(prefixToLibrary[codeRefChompedParts.first]);
} else {
var lookup = codeRefChompedParts.sublist(1).join('.');
prefixToLibrary[codeRefChompedParts.first]?.forEach((l) => l
.modelElementsNameMap[lookup]
?.map(_convertConstructors)
?.forEach((m) => _addCanonicalResult(m)));
}
}
}
void _findGlobalWithinRefElementCache() {
if (packageGraph.findRefElementCache.containsKey(codeRefChomped)) {
for (final modelElement
in packageGraph.findRefElementCache[codeRefChomped]) {
if (codeRefChomped == modelElement.fullyQualifiedNameWithoutLibrary ||
(modelElement is Library &&
codeRefChomped == modelElement.fullyQualifiedName)) {
_addCanonicalResult(_convertConstructors(modelElement));
}
}
}
}
void _findPartiallyQualifiedMatches() {
// Only look for partially qualified matches if we didn't find a fully qualified one.
if (library.modelElementsNameMap.containsKey(codeRefChomped)) {
for (final modelElement in library.modelElementsNameMap[codeRefChomped]) {
_addCanonicalResult(_convertConstructors(modelElement));
}
}
}
void _findWithinRefElementCache() {
// We now need the ref element cache to keep from repeatedly searching [Package.allModelElements].
// But if not, look for a fully qualified match. (That only makes sense
// if the codeRef might be qualified, and contains periods.)
if (codeRefChomped.contains('.') &&
packageGraph.findRefElementCache.containsKey(codeRefChomped)) {
for (var modelElement
in packageGraph.findRefElementCache[codeRefChomped]) {
_addCanonicalResult(_convertConstructors(modelElement));
}
}
}
void _findWithinTryClasses() {
// Maybe this is local to a class.
// TODO(jcollins-g): tryClasses is a strict subset of the superclass chain. Optimize.
var tryClasses = <Class>[preferredClass];
var realClass = tryClasses.first;
if (element is Inheritable) {
var overriddenElement = (element as Inheritable).overriddenElement;
while (overriddenElement != null) {
tryClasses
.add((element as Inheritable).overriddenElement.enclosingElement);
overriddenElement = overriddenElement.overriddenElement;
}
}
for (var tryClass in tryClasses) {
if (tryClass != null) {
if (codeRefChomped.contains('.') &&
!codeRefChomped.startsWith(tryClass.name)) {
continue;
}
_getResultsForClass(tryClass);
}
results.remove(null);
if (results.isNotEmpty) break;
}
if (results.isEmpty && realClass != null) {
for (var superClass
in realClass.publicSuperChain.map((et) => et.element)) {
if (!tryClasses.contains(superClass)) {
_getResultsForClass(superClass);
}
results.remove(null);
if (results.isNotEmpty) break;
}
}
}
void _findAnalyzerReferences() {
var refElement = _getRefElementFromCommentRefs(commentRefs, codeRef);
if (refElement == null) return;
ModelElement refModelElement;
if (refElement is MultiplyDefinedElement) {
var elementNames = refElement.conflictingElements
.map((e) => "'${_fullyQualifiedElementName(e)}'");
element.warn(PackageWarning.ambiguousDocReference,
message: '[$codeRef] => [${elementNames.join(', ')}]');
refModelElement = ModelElement.fromElement(
// Continue; just use the first conflicting element.
refElement.conflictingElements.first,
element.packageGraph);
} else {
refModelElement =
ModelElement.fromElement(refElement, element.packageGraph);
}
if (refModelElement is Accessor) {
refModelElement = (refModelElement as Accessor).enclosingCombo;
}
refModelElement = refModelElement.canonicalModelElement ?? refModelElement;
results.add(refModelElement);
}
/// Generates a fully-qualified name, similar to that of
/// [ModelElement.fullyQualifiedName], for an Element.
static String _fullyQualifiedElementName(Element element) {
var enclosingElement = element.enclosingElement;
var enclosingName = enclosingElement == null
? null
: _fullyQualifiedElementName(enclosingElement);
var name = element.name;
if (name == null) {
if (element is ExtensionElement) {
name = '<unnamed extension>';
} else if (element is LibraryElement) {
name = '<unnamed library>';
} else if (element is CompilationUnitElement) {
return enclosingName;
} else {
name = '<unnamed ${element.runtimeType}>';
}
}
return enclosingName == null ? name : '$enclosingName.$name';
}
// Add a result, but make it canonical.
void _addCanonicalResult(ModelElement modelElement) {
results.add(modelElement.canonicalModelElement);
}
/// _getResultsForClass assumes codeRefChomped might be a member of tryClass (inherited or not)
/// and will add to [results]
void _getResultsForClass(Class tryClass) {
// This might be part of the type arguments for the class, if so, add them.
// Otherwise, search the class.
if ((tryClass.modelType.typeArguments.map((e) => e.name))
.contains(codeRefChomped)) {
results.add((tryClass.modelType.typeArguments.firstWhere(
(e) => e.name == codeRefChomped && e is DefinedElementType)
as DefinedElementType)
.element);
} else {
// People like to use 'this' in docrefs too.
if (codeRef == 'this') {
_addCanonicalResult(tryClass);
} else {
// TODO(jcollins-g): get rid of reimplementation of identifier resolution
// or integrate into ModelElement in a simpler way.
var superChain = <Class>[tryClass];
superChain.addAll(tryClass.interfaces.map((t) => t.element));
// This seems duplicitous with our caller, but the preferredClass
// hint matters with findCanonicalModelElementFor.
// TODO(jcollins-g): This makes our caller ~O(n^2) vs length of superChain.
// Fortunately superChains are short, but optimize this if it matters.
superChain.addAll(tryClass.superChain.map((t) => t.element));
for (final c in superChain) {
_getResultsForSuperChainElement(c);
if (results.isNotEmpty) break;
}
}
}
}
/// Get any possible results for this class in the superChain. Returns
/// true if we found something.
void _getResultsForSuperChainElement(Class c) {
var membersToCheck = (c.allModelElementsByNamePart[codeRefChomped] ?? [])
.map(_convertConstructors);
for (var modelElement in membersToCheck) {
// [thing], a member of this class
_addCanonicalResult(modelElement);
}
if (codeRefChompedParts.length < 2 ||
codeRefChompedParts[codeRefChompedParts.length - 2] == c.name) {
membersToCheck =
(c.allModelElementsByNamePart[codeRefChompedParts.last] ??
<ModelElement>[])
.map(_convertConstructors);
membersToCheck.forEach((m) => _addCanonicalResult(m));
}
results.remove(null);
if (results.isNotEmpty) return;
if (c.fullyQualifiedNameWithoutLibrary == codeRefChomped) {
results.add(c);
}
}
}
md.Node _makeLinkNode(String codeRef, Warnable warnable,
List<ModelCommentReference> commentRefs) {
var result = _getMatchingLinkElement(codeRef, warnable, commentRefs);
var textContent = htmlEscape.convert(codeRef);
var linkedElement = result.element;
if (linkedElement != null) {
if (linkedElement.href != null) {
var anchor = md.Element.text('a', textContent);
if (linkedElement.isDeprecated) {
anchor.attributes['class'] = 'deprecated';
}
anchor.attributes['href'] = linkedElement.href;
return anchor;
}
// else this would be linkedElement.linkedName, but link bodies are slightly
// different for doc references, so fall out.
} else {
if (result.warn) {
// Avoid claiming documentation is inherited when it comes from the
// current element.
warnable.warn(PackageWarning.unresolvedDocReference,
message: codeRef,
referredFrom: warnable.documentationIsLocal
? null
: warnable.documentationFrom);
}
}
return md.Element.text('code', textContent);
}
// Maximum number of characters to display before a suspected generic.
const maxPriorContext = 20;
// Maximum number of characters to display after the beginning of a suspected generic.
const maxPostContext = 30;
final RegExp allBeforeFirstNewline = RegExp(r'^.*\n', multiLine: true);
final RegExp allAfterLastNewline = RegExp(r'\n.*$', multiLine: true);
// Generics should be wrapped into `[]` blocks, to avoid handling them as HTML tags
// (like, [Apple<int>]). @Hixie asked for a warning when there's something, that looks
// like a non HTML tag (a generic?) outside of a `[]` block.
// https://github.com/dart-lang/dartdoc/issues/1250#issuecomment-269257942
void showWarningsForGenericsOutsideSquareBracketsBlocks(
String text, Warnable element) {
var tagPositions = findFreeHangingGenericsPositions(text);
if (tagPositions.isNotEmpty) {
tagPositions.forEach((int position) {
var priorContext =
'${text.substring(max(position - maxPriorContext, 0), position)}';
var postContext =
'${text.substring(position, min(position + maxPostContext, text.length))}';
priorContext = priorContext.replaceAll(allBeforeFirstNewline, '');
postContext = postContext.replaceAll(allAfterLastNewline, '');
var errorMessage = '$priorContext$postContext';
// TODO(jcollins-g): allow for more specific error location inside comments
element.warn(PackageWarning.typeAsHtml, message: errorMessage);
});
}
}
List<int> findFreeHangingGenericsPositions(String string) {
var currentPosition = 0;
var squareBracketsDepth = 0;
var results = <int>[];
while (true) {
var nextOpenBracket = string.indexOf('[', currentPosition);
var nextCloseBracket = string.indexOf(']', currentPosition);
var nextNonHTMLTag = string.indexOf(nonHTML, currentPosition);
var nextPositions = [nextOpenBracket, nextCloseBracket, nextNonHTMLTag]
.where((p) => p != -1);
if (nextPositions.isNotEmpty) {
final minPos = nextPositions.reduce(min);
if (nextOpenBracket == minPos) {
squareBracketsDepth += 1;
} else if (nextCloseBracket == minPos) {
squareBracketsDepth = max(squareBracketsDepth - 1, 0);
} else if (nextNonHTMLTag == minPos) {
if (squareBracketsDepth == 0) {
results.add(minPos);
}
}
currentPosition = minPos + 1;
} else {
break;
}
}
return results;
}
class MarkdownDocument extends md.Document {
factory MarkdownDocument.withElementLinkResolver(
Canonicalization element, List<ModelCommentReference> commentRefs) {
md.Node linkResolver(String name, [String _]) {
if (name.isEmpty) {
return null;
}
return _makeLinkNode(name, element, commentRefs);
}
return MarkdownDocument(
inlineSyntaxes: _markdown_syntaxes,
blockSyntaxes: _markdown_block_syntaxes,
linkResolver: linkResolver);
}
MarkdownDocument(
{Iterable<md.BlockSyntax> blockSyntaxes,
Iterable<md.InlineSyntax> inlineSyntaxes,
md.ExtensionSet extensionSet,
md.Resolver linkResolver,
md.Resolver imageLinkResolver})
: super(
blockSyntaxes: blockSyntaxes,
inlineSyntaxes: inlineSyntaxes,
extensionSet: extensionSet,
linkResolver: linkResolver,
imageLinkResolver: imageLinkResolver);
/// Returns a tuple of List<md.Node> and hasExtendedContent
Tuple2<List<md.Node>, bool> parseMarkdownText(
String text, bool processFullText) {
var hasExtendedContent = false;
var lines = LineSplitter.split(text).toList();
md.Node firstNode;
var nodes = <md.Node>[];
for (var node in IterableBlockParser(lines, this).parseLinesGenerator()) {
if (firstNode != null) {
hasExtendedContent = true;
if (!processFullText) break;
}
firstNode ??= node;
nodes.add(node);
}
_parseInlineContent(nodes);
return Tuple2(nodes, hasExtendedContent);
}
// From package:markdown/src/document.dart
// TODO(jcollins-g): consider making this a public method in markdown package
void _parseInlineContent(List<md.Node> nodes) {
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node is md.UnparsedContent) {
var inlineNodes = md.InlineParser(node.textContent, this).parse();
nodes.removeAt(i);
nodes.insertAll(i, inlineNodes);
i += inlineNodes.length - 1;
} else if (node is md.Element && node.children != null) {
_parseInlineContent(node.children);
}
}
}
}
class _InlineCodeSyntax extends md.InlineSyntax {
_InlineCodeSyntax() : super(r'\[:\s?((?:.|\n)*?)\s?:\]');
@override
bool onMatch(md.InlineParser parser, Match match) {
var element = md.Element.text('code', htmlEscape.convert(match[1]));
parser.addNode(element);
return true;
}
}
class _AutolinkWithoutScheme extends md.AutolinkSyntax {
@override
bool onMatch(md.InlineParser parser, Match match) {
var url = match[1];
var text = htmlEscape.convert(url).replaceFirst(_hide_schemes, '');
var anchor = md.Element.text('a', text);
anchor.attributes['href'] = url;
parser.addNode(anchor);
return true;
}
}