blob: 093f6647d1a5aab297ad80c6d5863d79e2bb96d2 [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/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/dart/element/member.dart' show Member;
import 'package:html/parser.dart' show parse;
import 'package:markdown/markdown.dart' as md;
import 'model.dart';
const validHtmlTags = const [
"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 =
new RegExp("</?(?!(${validHtmlTags.join("|")})[> ])\\w+[> ]");
// Type parameters and other things to ignore at the end of doc references.
final RegExp trailingIgnoreStuff = new RegExp(r'(<.*>|\(.*\))$');
// Things to ignore at the beginning of doc references
final RegExp leadingIgnoreStuff =
new RegExp(r'^(const|final|var)[\s]+', multiLine: true);
// This is explicitly intended as a reference to a constructor.
final RegExp isConstructor = new RegExp(r'^new[\s]+', multiLine: true);
// This is probably not really intended as a doc reference, so don't try or
// warn about them.
// Covers anything with leading digits/symbols, empty string, weird punctuation, spaces.
final RegExp notARealDocReference = new RegExp(r'''(^[^\w]|^[\d]|[,"'/]|^$)''');
// We don't emit warnings currently: #572.
const List<String> _oneLinerSkipTags = const ["code", "pre"];
final List<md.InlineSyntax> _markdown_syntaxes = [
new _InlineCodeSyntax(),
new _AutolinkWithoutScheme()
];
// Remove these schemas from the display text for hyperlinks.
final RegExp _hide_schemes = new RegExp('^(http|https)://');
class MatchingLinkResult {
final ModelElement element;
final String label;
final bool warn;
MatchingLinkResult(this.element, this.label, {this.warn: true});
}
// Calculate a class hint for findCanonicalModelElementFor.
ModelElement _getPreferredClass(ModelElement modelElement) {
if (modelElement is EnclosedElement &&
(modelElement as EnclosedElement).enclosingElement is Class) {
return (modelElement as EnclosedElement).enclosingElement;
} else if (modelElement is Class) {
return modelElement;
}
return null;
}
// TODO: this is in the wrong place
NodeList<CommentReference> _getCommentRefs(ModelElement modelElement) {
Class preferredClass = _getPreferredClass(modelElement);
ModelElement cModelElement = modelElement.package
.findCanonicalModelElementFor(modelElement.element,
preferredClass: preferredClass);
if (cModelElement == null) return null;
modelElement = cModelElement;
if (modelElement.element.documentationComment == null &&
modelElement.canOverride()) {
var node = modelElement.overriddenElement?.element?.computeNode();
if (node is AnnotatedNode) {
if (node.documentationComment != null) {
return node.documentationComment.references;
}
}
}
if (modelElement.element.computeNode() is AnnotatedNode) {
final AnnotatedNode annotatedNode = modelElement.element.computeNode();
if (annotatedNode.documentationComment != null) {
return annotatedNode.documentationComment.references;
}
} else if (modelElement.element is LibraryElement) {
// handle anonymous libraries
if (modelElement.element.computeNode() == null ||
modelElement.element.computeNode().parent == null) {
return null;
}
var node = modelElement.element.computeNode().parent.parent;
if (node is AnnotatedNode) {
if (node.documentationComment != null) {
return node.documentationComment.references;
}
}
}
// Our references might come from somewhere up in the inheritance chain.
// TODO(jcollins-g): rationalize this and all other places where docs are
// inherited to be consistent.
if (modelElement.element is ClassMemberElement) {
var node = modelElement.element
.getAncestor((e) => e is ClassElement)
.computeNode();
if (node is AnnotatedNode) {
if (node.documentationComment != null) {
return node.documentationComment.references;
}
}
}
return null;
}
/// Returns null if element is a parameter.
MatchingLinkResult _getMatchingLinkElement(
String codeRef, ModelElement element, List<CommentReference> commentRefs) {
// By debugging inspection, it seems correct to not warn when we don't have
// CommentReferences; there's actually nothing that needs resolving in
// that case.
if (commentRefs == null)
return new MatchingLinkResult(null, null, warn: false);
if (!codeRef.contains(isConstructor) &&
codeRef.contains(notARealDocReference)) {
// Don't waste our time on things we won't ever find.
return new MatchingLinkResult(null, null, warn: false);
}
Element refElement;
// Try expensive not-scoped lookup.
if (refElement == null) {
refElement = _findRefElementInLibrary(codeRef, element, commentRefs);
}
// This is faster but does not take canonicalization into account; try
// only as a last resort. TODO(jcollins-g): make analyzer comment references
// dartdoc-canonicalization-aware?
if (refElement == null) {
refElement = _getRefElementFromCommentRefs(commentRefs, codeRef);
}
// Did not find it anywhere.
if (refElement == null) {
return new MatchingLinkResult(null, null);
}
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;
}
// Ignore all parameters.
if (refElement is ParameterElement || refElement is TypeParameterElement)
return new MatchingLinkResult(null, null, warn: false);
Library refLibrary = element.package.findOrCreateLibraryFor(refElement);
Element searchElement =
refElement is Member ? refElement.baseElement : refElement;
Class preferredClass = _getPreferredClass(element);
ModelElement refModelElement = element.package.findCanonicalModelElementFor(
searchElement,
preferredClass: preferredClass);
// 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.package == element.package);
if (refModelElement != null) {
return new MatchingLinkResult(refModelElement, null);
}
refModelElement = new ModelElement.from(searchElement, refLibrary);
if (!refModelElement.isCanonical) {
refModelElement.warn(PackageWarning.noCanonicalFound);
// Don't warn about doc references because that's covered by the no
// canonical library found message.
return new MatchingLinkResult(null, 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 new MatchingLinkResult(refModelElement, null);
}
/// Given a set of commentRefs, return the one whose name matches the codeRef.
Element _getRefElementFromCommentRefs(
List<CommentReference> commentRefs, String codeRef) {
for (CommentReference ref in commentRefs) {
if (ref.identifier.name == codeRef) {
bool isConstrElement = ref.identifier.staticElement is ConstructorElement;
// Constructors are now handled by library search.
if (!isConstrElement) {
return ref.identifier.staticElement;
}
}
}
return null;
}
/// Returns true if this is a constructor we should consider in
/// _findRefElementInLibrary, or if this isn't a constructor.
bool _ConsiderIfConstructor(String codeRef, ModelElement modelElement) {
if (modelElement is! Constructor) return true;
if (codeRef.contains(isConstructor)) return true;
Constructor aConstructor = modelElement;
List<String> codeRefParts = codeRef.split('.');
if (codeRefParts.length > 1) {
if (codeRefParts[codeRefParts.length - 1] ==
codeRefParts[codeRefParts.length - 2]) {
// Foobar.Foobar -- assume they really do mean the constructor for this class.
return true;
}
}
if (aConstructor.name != aConstructor.enclosingElement.name) {
// This isn't a default constructor so treat it like any other member.
return true;
}
return false;
}
// Basic map of reference to ModelElement, for cases where we're searching
// outside of scope.
// TODO(jcollins-g): function caches with maps are very common in dartdoc.
// Extract into library.
Map<String, Set<ModelElement>> _findRefElementCache;
// TODO(jcollins-g): Rewrite this to handle constructors in a less hacky way
// TODO(jcollins-g): This function breaks down naturally into many helpers, extract them
// TODO(jcollins-g): Subcomponents of this function shouldn't be adding nulls to results, strip the
// removes out that are gratuitous and debug the individual pieces.
// TODO(jcollins-g): A complex package winds up spending a lot of cycles in here. Optimize.
Element _findRefElementInLibrary(
String codeRef, ModelElement element, List<CommentReference> commentRefs) {
assert(element.package.allLibrariesAdded);
String codeRefChomped = codeRef.replaceFirst(isConstructor, '');
final Library library = element.library;
final Package package = library.package;
final Set<ModelElement> results = new Set();
// This might be an operator. Strip the operator prefix and try again.
if (results.isEmpty && codeRef.startsWith('operator')) {
String newCodeRef = codeRef.replaceFirst('operator', '');
return _findRefElementInLibrary(newCodeRef, element, commentRefs);
}
results.remove(null);
// Oh, and someone might have some type parameters or other garbage.
if (results.isEmpty && codeRef.contains(trailingIgnoreStuff)) {
String newCodeRef = codeRef.replaceFirst(trailingIgnoreStuff, '');
return _findRefElementInLibrary(newCodeRef, element, commentRefs);
}
results.remove(null);
// Oh, and someone might have thrown on a 'const' or 'final' in front.
if (results.isEmpty && codeRef.contains(leadingIgnoreStuff)) {
String newCodeRef = codeRef.replaceFirst(leadingIgnoreStuff, '');
return _findRefElementInLibrary(newCodeRef, element, commentRefs);
}
// 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.
// TODO(jcollins-g): link to classes that are the types of parameters, where known
results.addAll(element.allParameters.where((p) =>
p.name == codeRefChomped || codeRefChomped.startsWith("${p.name}.")));
results.remove(null);
if (results.isEmpty) {
// Maybe this is local to a class.
// TODO(jcollins-g): tryClasses is a strict subset of the superclass chain. Optimize.
List<Class> tryClasses = [_getPreferredClass(element)];
Class realClass = tryClasses.first;
if (element is Inheritable) {
ModelElement overriddenElement = element.overriddenElement;
while (overriddenElement != null) {
tryClasses.add(
(element.overriddenElement as EnclosedElement).enclosingElement);
overriddenElement = overriddenElement.overriddenElement;
}
}
for (Class tryClass in tryClasses) {
if (tryClass != null) {
_getResultsForClass(
tryClass, codeRefChomped, results, codeRef, package);
}
if (results.isNotEmpty) break;
}
// Sometimes documentation refers to classes that are further up the chain.
// Get those too.
if (results.isEmpty && realClass != null) {
for (Class superClass
in realClass.superChain.map((et) => et.element as Class)) {
if (!tryClasses.contains(superClass)) {
_getResultsForClass(
superClass, codeRefChomped, results, codeRef, package);
}
if (results.isNotEmpty) break;
}
}
}
results.remove(null);
// We now need the ref element cache to keep from repeatedly searching [Package.allModelElements].
// TODO(jcollins-g): Find somewhere to cache elements outside package.libraries
// so we can give the right warning (no canonical found)
// when referring to objects in libraries outside the
// documented set.
if (results.isEmpty && _findRefElementCache == null) {
assert(package.allLibrariesAdded);
_findRefElementCache = new Map();
for (final modelElement in package.allModelElements) {
_findRefElementCache.putIfAbsent(
modelElement.fullyQualifiedNameWithoutLibrary, () => new Set());
_findRefElementCache.putIfAbsent(
modelElement.fullyQualifiedName, () => new Set());
_findRefElementCache[modelElement.fullyQualifiedName].add(modelElement);
_findRefElementCache[modelElement.fullyQualifiedNameWithoutLibrary]
.add(modelElement);
}
}
// But if not, look for a fully qualified match. (That only makes sense
// if the codeRef might be qualified, and contains periods.)
if (results.isEmpty &&
codeRefChomped.contains('.') &&
_findRefElementCache.containsKey(codeRefChomped)) {
for (final modelElement in _findRefElementCache[codeRefChomped]) {
if (!_ConsiderIfConstructor(codeRef, modelElement)) continue;
results.add(package.findCanonicalModelElementFor(modelElement.element));
}
}
results.remove(null);
// Only look for partially qualified matches if we didn't find a fully qualified one.
if (results.isEmpty) {
for (final modelElement in library.allModelElements) {
if (!_ConsiderIfConstructor(codeRef, modelElement)) continue;
if (codeRefChomped == modelElement.fullyQualifiedNameWithoutLibrary) {
results.add(package.findCanonicalModelElementFor(modelElement.element));
}
}
}
results.remove(null);
// And if we still haven't found anything, just search the whole ball-of-wax.
if (results.isEmpty && _findRefElementCache.containsKey(codeRefChomped)) {
for (final modelElement in _findRefElementCache[codeRefChomped]) {
if (codeRefChomped == modelElement.fullyQualifiedNameWithoutLibrary ||
(modelElement is Library &&
codeRefChomped == modelElement.fullyQualifiedName)) {
results.add(package.findCanonicalModelElementFor(modelElement.element));
}
}
}
// This could conceivably be a reference to an enum member. They don't show up in allModelElements.
// 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 (results.isEmpty) {
List<String> codeRefChompedParts = codeRefChomped.split('.');
if (codeRefChompedParts.length >= 2) {
String maybeEnumName = codeRefChompedParts
.sublist(0, codeRefChompedParts.length - 1)
.join('.');
String maybeEnumMember = codeRefChompedParts.last;
if (_findRefElementCache.containsKey(maybeEnumName)) {
for (final modelElement in _findRefElementCache[maybeEnumName]) {
if (modelElement is Enum) {
if (modelElement.constants.any((e) => e.name == maybeEnumMember)) {
results.add(modelElement);
break;
}
}
}
}
}
}
Element result;
results.remove(null);
if (results.length > 1) {
// If this name could refer to a class or a constructor, prefer the class.
if (results.any((r) => r is Class)) {
results.removeWhere((r) => r is Constructor);
}
}
if (results.length > 1) {
// Attempt to disambiguate using the library.
// TODO(jcollins-g): we could have saved ourselves some work by using the analyzer
// to search the namespace, somehow. Do that instead.
if (results.any((r) => r.element.isAccessibleIn(element.library.element))) {
results.removeWhere(
(r) => !r.element.isAccessibleIn(element.library.element));
}
}
// TODO(jcollins-g): This is only necessary because we had to jettison commentRefs
// as a way to figure this out. We could reintroduce commentRefs, or we could
// compute this via other means.
if (results.length > 1) {
String startName = "${element.fullyQualifiedName}.";
String 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));
}
}
// TODO(jcollins-g): As a last resort, try disambiguation with commentRefs.
// Maybe one of these is the same as what's resolvable with
// the analyzer, and if so, drop the others. We can't
// do this in reverse order because commentRefs don't know
// about dartdoc canonicalization.
if (results.length > 1) {
Element refElement = _getRefElementFromCommentRefs(commentRefs, codeRef);
if (results.any((me) => me.element == refElement)) {
results.removeWhere((me) => me.element != refElement);
}
}
// TODO(jcollins-g): further disambiguations based on package information?
if (results.isEmpty) {
result = null;
} else if (results.length == 1) {
result = results.first.element;
} else {
element.warn(PackageWarning.ambiguousDocReference,
"[$codeRef] => ${results.map((r) => "'${r.fullyQualifiedName}'").join(", ")}");
result = results.first.element;
}
return result;
}
// _getResultsForClass assumes codeRefChomped might be a member of tryClass (inherited or not)
// and will add to [results]
void _getResultsForClass(Class tryClass, String codeRefChomped,
Set<ModelElement> results, String codeRef, Package package) {
// 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)
.element);
} else {
// People like to use 'this' in docrefs too.
if (codeRef == 'this') {
results.add(package.findCanonicalModelElementFor(tryClass.element));
} else {
// TODO(jcollins-g): get rid of reimplementation of identifier resolution
// or integrate into ModelElement in a simpler way.
List<Class> superChain = [];
superChain.add(tryClass);
// 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.superChainRaw.map((t) => t.returnElement as Class));
List<String> codeRefParts = codeRefChomped.split('.');
for (final c in superChain) {
// TODO(jcollins-g): add a hash-map-enabled lookup function to Class?
for (final modelElement in c.allModelElements) {
if (!_ConsiderIfConstructor(codeRef, modelElement)) continue;
String namePart = modelElement.fullyQualifiedName.split('.').last;
// TODO(jcollins-g): fix operators so we can use 'name' here or similar.
if (codeRefChomped == namePart) {
results.add(package.findCanonicalModelElementFor(
modelElement.element,
preferredClass: tryClass));
continue;
}
// Handle non-documented class documentation being imported into a
// documented class when it refers to itself (with help from caller's
// iteration on tryClasses).
// TODO(jcollins-g): Fix partial qualifications in _findRefElementInLibrary so it can tell
// when it is referenced from a non-documented element?
// TODO(jcollins-g): We could probably check this early.
if (codeRefParts.first == c.name && codeRefParts.last == namePart) {
results.add(package.findCanonicalModelElementFor(
modelElement.element,
preferredClass: tryClass));
continue;
}
if (modelElement is Constructor) {
// Constructor names don't include the class, so we might miss them in the above search.
List<String> codeRefParts = codeRefChomped.split('.');
if (codeRefParts.length > 1) {
String codeRefClass = codeRefParts[codeRefParts.length - 2];
String codeRefConstructor = codeRefParts.last;
if (codeRefClass == c.name &&
codeRefConstructor ==
modelElement.fullyQualifiedName.split('.').last) {
results.add(package.findCanonicalModelElementFor(
modelElement.element,
preferredClass: tryClass));
continue;
}
}
}
}
if (results.isNotEmpty) break;
if (c.fullyQualifiedNameWithoutLibrary == codeRefChomped) {
results.add(c);
break;
}
}
}
}
}
String _linkDocReference(String codeRef, ModelElement element,
NodeList<CommentReference> commentRefs) {
// TODO(jcollins-g): Refactor so that doc operations work on the
// documented element.
element = element.overriddenDocumentedElement;
MatchingLinkResult result;
result = _getMatchingLinkElement(codeRef, element, commentRefs);
final ModelElement linkedElement = result.element;
final String label = result.label ?? codeRef;
if (linkedElement != null) {
var classContent = '';
if (linkedElement.isDeprecated) {
classContent = 'class="deprecated" ';
}
// This would be linkedElement.linkedName, but link bodies are slightly
// different for doc references.
if (linkedElement.href == null) {
return '<code>${HTML_ESCAPE.convert(label)}</code>';
} else {
return '<a ${classContent}href="${linkedElement.href}">$label</a>';
}
} else {
if (result.warn) {
element.warn(PackageWarning.unresolvedDocReference, codeRef);
}
return '<code>${HTML_ESCAPE.convert(label)}</code>';
}
}
String _renderMarkdownToHtml(String text, [ModelElement element]) {
md.Node _linkResolver(String name) {
NodeList<CommentReference> commentRefs = _getCommentRefs(element);
return new md.Text(_linkDocReference(name, element, commentRefs));
}
_showWarningsForGenericsOutsideSquareBracketsBlocks(text, element);
return md.markdownToHtml(text,
inlineSyntaxes: _markdown_syntaxes, linkResolver: _linkResolver);
}
// 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;
// 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, ModelElement element) {
List<int> tagPositions = findFreeHangingGenericsPositions(text);
if (tagPositions.isNotEmpty) {
tagPositions.forEach((int position) {
String priorContext =
"${text.substring(max(position - maxPriorContext, 0), position)}";
String postContext =
"${text.substring(position, min(position + maxPostContext, text.length))}";
priorContext =
priorContext.replaceAll(new RegExp(r'^.*\n', multiLine: true), '');
postContext =
postContext.replaceAll(new RegExp(r'\n.*$', multiLine: true), '');
String errorMessage = "$priorContext$postContext";
// TODO(jcollins-g): allow for more specific error location inside comments
element.warn(PackageWarning.typeAsHtml, errorMessage);
});
}
}
List<int> findFreeHangingGenericsPositions(String string) {
int currentPosition = 0;
int squareBracketsDepth = 0;
List<int> results = [];
while (true) {
final int nextOpenBracket = string.indexOf("[", currentPosition);
final int nextCloseBracket = string.indexOf("]", currentPosition);
final int nextNonHTMLTag = string.indexOf(nonHTML, currentPosition);
final Iterable<int> 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 Documentation {
final String raw;
final String asHtml;
final String asOneLiner;
factory Documentation(String markdown) {
String tempHtml = _renderMarkdownToHtml(markdown);
return new Documentation._internal(markdown, tempHtml);
}
factory Documentation.forElement(ModelElement element) {
String tempHtml = _renderMarkdownToHtml(element.documentation, element);
return new Documentation._internal(element.documentation, tempHtml);
}
Documentation._(this.raw, this.asHtml, this.asOneLiner);
factory Documentation._internal(String markdown, String rawHtml) {
var asHtmlDocument = parse(rawHtml);
for (var s in asHtmlDocument.querySelectorAll('script')) {
s.remove();
}
for (var pre in asHtmlDocument.querySelectorAll('pre')) {
if (pre.children.isNotEmpty &&
pre.children.length != 1 &&
pre.children.first.localName != 'code') {
continue;
}
if (pre.children.isNotEmpty && pre.children.first.localName == 'code') {
var code = pre.children.first;
pre.classes
.addAll(code.classes.where((name) => name.startsWith('language-')));
}
bool specifiesLanguage = pre.classes.isNotEmpty;
pre.classes.add('prettyprint');
// Assume the user intended Dart if there are no other classes present.
if (!specifiesLanguage) pre.classes.add('language-dart');
}
// `trim` fixes issue with line ending differences between mac and windows.
var asHtml = asHtmlDocument.body.innerHtml?.trim();
var asOneLiner = asHtmlDocument.body.children.isEmpty
? ''
: asHtmlDocument.body.children.first.innerHtml;
if (!asOneLiner.startsWith('<p>')) {
asOneLiner = '<p>$asOneLiner</p>';
}
return new Documentation._(markdown, asHtml, asOneLiner);
}
}
class _InlineCodeSyntax extends md.InlineSyntax {
_InlineCodeSyntax() : super(r'\[:\s?((?:.|\n)*?)\s?:\]');
@override
bool onMatch(md.InlineParser parser, Match match) {
var element = new md.Element.text('code', HTML_ESCAPE.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 = md.escapeHtml(url).replaceFirst(_hide_schemes, '');
var anchor = new md.Element.text('a', text);
anchor.attributes['href'] = url;
parser.addNode(anchor);
return true;
}
}