| // 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; |
| } |
| } |