blob: 210e84cc208ecd3abd70afe226cc8eb10241e05e [file] [log] [blame]
// Copyright (c) 2012, 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.
import "dart:collection";
import 'dart:convert';
import 'dart:html';
// Workaround for HTML lib missing feature.
Range newRange() {
return document.createRange();
}
// Temporary range object to optimize performance computing client rects
// from text nodes.
Range _tempRange;
// Hacks because ASYNC measurement is annoying when just writing a script.
ClientRect getClientRect(Node n) {
if (n is Element) {
return n.$dom_getBoundingClientRect();
} else {
// Crazy hacks that works for nodes.... create a range and measure it.
if (_tempRange == null) {
_tempRange = newRange();
}
_tempRange.setStartBefore(n);
_tempRange.setEndAfter(n);
return _tempRange.getBoundingClientRect();
}
}
/**
* CSS class that is added to elements in the DOM to indicate that they should
* be removed when extracting blocks of documentation. This is helpful when
* running this script in a web browser as it is easy to visually see what
* blocks of information were extracted when using CSS such as DEBUG_CSS
* which highlights elements that should be removed.
*/
const DART_REMOVED = "dart-removed";
const DEBUG_CSS = """
<style type="text/css">
.dart-removed {
background-color: rgba(255, 0, 0, 0.5);
}
</style>""";
const MIN_PIXELS_DIFFERENT_LINES = 10;
const IDL_SELECTOR = "pre.eval, pre.idl";
Map data;
// TODO(rnystrom): Hack! Copied from domTypes.json. Instead of hard-coding
// these, should use the same mapping that the DOM/HTML code generators use.
var domTypes;
const domTypesRaw = const [
"AbstractWorker", "ArrayBuffer", "ArrayBufferView", "Attr",
"AudioBuffer", "AudioBufferSourceNode", "AudioChannelMerger",
"AudioChannelSplitter", "AudioContext", "AudioDestinationNode",
"AudioGain", "AudioGainNode", "AudioListener", "AudioNode",
"AudioPannerNode", "AudioParam", "AudioProcessingEvent",
"AudioSourceNode", "BarInfo", "BeforeLoadEvent", "BiquadFilterNode",
"Blob", "CDATASection", "CSSCharsetRule", "CSSFontFaceRule",
"CSSImportRule", "CSSMediaRule", "CSSPageRule", "CSSPrimitiveValue",
"CSSRule", "CSSRuleList", "CSSStyleDeclaration", "CSSStyleRule",
"CSSStyleSheet", "CSSUnknownRule", "CSSValue", "CSSValueList",
"CanvasGradient", "CanvasPattern", "CanvasPixelArray",
"CanvasRenderingContext", "CanvasRenderingContext2D",
"CharacterData", "ClientRect", "ClientRectList", "Clipboard",
"CloseEvent", "Comment", "CompositionEvent", "Console",
"ConvolverNode", "Coordinates", "Counter", "Crypto", "CustomEvent",
"DOMApplicationCache", "DOMException", "DOMFileSystem",
"DOMFileSystemSync", "DOMFormData", "DOMImplementation",
"DOMMimeType", "DOMMimeTypeArray", "DOMParser", "DOMPlugin",
"DOMPluginArray", "DOMSelection", "DOMSettableTokenList",
"DOMTokenList", "DOMURL", "DOMWindow", "DataTransferItem",
"DataTransferItemList", "DataView", "Database", "DatabaseSync",
"DedicatedWorkerContext", "DelayNode", "DeviceMotionEvent",
"DeviceOrientationEvent", "DirectoryEntry", "DirectoryEntrySync",
"DirectoryReader", "DirectoryReaderSync", "Document",
"DocumentFragment", "DocumentType", "DynamicsCompressorNode",
"Element", "ElementTimeControl", "ElementTraversal", "Entity",
"EntityReference", "Entry", "EntryArray", "EntryArraySync",
"EntrySync", "ErrorEvent", "Event", "EventException", "EventSource",
"EventTarget", "File", "FileEntry", "FileEntrySync", "FileError",
"FileException", "FileList", "FileReader", "FileReaderSync",
"FileWriter", "FileWriterSync", "Float32Array", "Float64Array",
"Geolocation", "Geoposition", "HTMLAllCollection",
"HTMLAnchorElement", "HTMLAppletElement", "HTMLAreaElement",
"HTMLAudioElement", "HTMLBRElement", "HTMLBaseElement",
"HTMLBaseFontElement", "HTMLBodyElement", "HTMLButtonElement",
"HTMLCanvasElement", "HTMLCollection", "HTMLDListElement",
"HTMLDataListElement", "HTMLDetailsElement", "HTMLDirectoryElement",
"HTMLDivElement", "HTMLDocument", "HTMLElement", "HTMLEmbedElement",
"HTMLFieldSetElement", "HTMLFontElement", "HTMLFormElement",
"HTMLFrameElement", "HTMLFrameSetElement", "HTMLHRElement",
"HTMLHeadElement", "HTMLHeadingElement", "HTMLHtmlElement",
"HTMLIFrameElement", "HTMLImageElement", "HTMLInputElement",
"HTMLIsIndexElement", "HTMLKeygenElement", "HTMLLIElement",
"HTMLLabelElement", "HTMLLegendElement", "HTMLLinkElement",
"HTMLMapElement", "HTMLMarqueeElement", "HTMLMediaElement",
"HTMLMenuElement", "HTMLMetaElement", "HTMLMeterElement",
"HTMLModElement", "HTMLOListElement", "HTMLObjectElement",
"HTMLOptGroupElement", "HTMLOptionElement", "HTMLOptionsCollection",
"HTMLOutputElement", "HTMLParagraphElement", "HTMLParamElement",
"HTMLPreElement", "HTMLProgressElement", "HTMLQuoteElement",
"HTMLScriptElement", "HTMLSelectElement", "HTMLSourceElement",
"HTMLSpanElement", "HTMLStyleElement", "HTMLTableCaptionElement",
"HTMLTableCellElement", "HTMLTableColElement", "HTMLTableElement",
"HTMLTableRowElement", "HTMLTableSectionElement",
"HTMLTextAreaElement", "HTMLTitleElement", "HTMLTrackElement",
"HTMLUListElement", "HTMLUnknownElement", "HTMLVideoElement",
"HashChangeEvent", "HighPass2FilterNode", "History", "IDBAny",
"IDBCursor", "IDBCursorWithValue", "IDBDatabase",
"IDBDatabaseError", "IDBDatabaseException", "IDBFactory",
"IDBIndex", "IDBKey", "IDBKeyRange", "IDBObjectStore", "IDBRequest",
"IDBTransaction", "IDBVersionChangeEvent",
"IDBVersionChangeRequest", "ImageData", "InjectedScriptHost",
"InspectorFrontendHost", "Int16Array", "Int32Array", "Int8Array",
"JavaScriptAudioNode", "JavaScriptCallFrame", "KeyboardEvent",
"Location", "LowPass2FilterNode", "MediaElementAudioSourceNode",
"MediaError", "MediaList", "MediaQueryList",
"MediaQueryListListener", "MemoryInfo", "MessageChannel",
"MessageEvent", "MessagePort", "Metadata", "MouseEvent",
"MutationCallback", "MutationEvent", "MutationRecord",
"NamedNodeMap", "Navigator", "NavigatorUserMediaError",
"NavigatorUserMediaSuccessCallback", "Node", "NodeFilter",
"NodeIterator", "NodeList", "NodeSelector", "Notation",
"Notification", "NotificationCenter", "OESStandardDerivatives",
"OESTextureFloat", "OESVertexArrayObject",
"OfflineAudioCompletionEvent", "OperationNotAllowedException",
"OverflowEvent", "PageTransitionEvent", "Performance",
"PerformanceNavigation", "PerformanceTiming", "PopStateEvent",
"PositionError", "ProcessingInstruction", "ProgressEvent",
"RGBColor", "Range", "RangeException", "RealtimeAnalyserNode",
"Rect", "SQLError", "SQLException", "SQLResultSet",
"SQLResultSetRowList", "SQLTransaction", "SQLTransactionSync",
"SVGAElement", "SVGAltGlyphDefElement", "SVGAltGlyphElement",
"SVGAltGlyphItemElement", "SVGAngle", "SVGAnimateColorElement",
"SVGAnimateElement", "SVGAnimateMotionElement",
"SVGAnimateTransformElement", "SVGAnimatedAngle",
"SVGAnimatedBoolean", "SVGAnimatedEnumeration",
"SVGAnimatedInteger", "SVGAnimatedLength", "SVGAnimatedLengthList",
"SVGAnimatedNumber", "SVGAnimatedNumberList",
"SVGAnimatedPreserveAspectRatio", "SVGAnimatedRect",
"SVGAnimatedString", "SVGAnimatedTransformList",
"SVGAnimationElement", "SVGCircleElement", "SVGClipPathElement",
"SVGColor", "SVGComponentTransferFunctionElement",
"SVGCursorElement", "SVGDefsElement", "SVGDescElement",
"SVGDocument", "SVGElement", "SVGElementInstance",
"SVGElementInstanceList", "SVGEllipseElement", "SVGException",
"SVGExternalResourcesRequired", "SVGFEBlendElement",
"SVGFEColorMatrixElement", "SVGFEComponentTransferElement",
"SVGFECompositeElement", "SVGFEConvolveMatrixElement",
"SVGFEDiffuseLightingElement", "SVGFEDisplacementMapElement",
"SVGFEDistantLightElement", "SVGFEDropShadowElement",
"SVGFEFloodElement", "SVGFEFuncAElement", "SVGFEFuncBElement",
"SVGFEFuncGElement", "SVGFEFuncRElement",
"SVGFEGaussianBlurElement", "SVGFEImageElement",
"SVGFEMergeElement", "SVGFEMergeNodeElement",
"SVGFEMorphologyElement", "SVGFEOffsetElement",
"SVGFEPointLightElement", "SVGFESpecularLightingElement",
"SVGFESpotLightElement", "SVGFETileElement",
"SVGFETurbulenceElement", "SVGFilterElement",
"SVGFilterPrimitiveStandardAttributes", "SVGFitToViewBox",
"SVGFontElement", "SVGFontFaceElement", "SVGFontFaceFormatElement",
"SVGFontFaceNameElement", "SVGFontFaceSrcElement",
"SVGFontFaceUriElement", "SVGForeignObjectElement", "SVGGElement",
"SVGGlyphElement", "SVGGlyphRefElement", "SVGGradientElement",
"SVGHKernElement", "SVGImageElement", "SVGLangSpace", "SVGLength",
"SVGLengthList", "SVGLineElement", "SVGLinearGradientElement",
"SVGLocatable", "SVGMPathElement", "SVGMarkerElement",
"SVGMaskElement", "SVGMatrix", "SVGMetadataElement",
"SVGMissingGlyphElement", "SVGNumber", "SVGNumberList", "SVGPaint",
"SVGPathElement", "SVGPathSeg", "SVGPathSegArcAbs",
"SVGPathSegArcRel", "SVGPathSegClosePath",
"SVGPathSegCurvetoCubicAbs", "SVGPathSegCurvetoCubicRel",
"SVGPathSegCurvetoCubicSmoothAbs",
"SVGPathSegCurvetoCubicSmoothRel", "SVGPathSegCurvetoQuadraticAbs",
"SVGPathSegCurvetoQuadraticRel",
"SVGPathSegCurvetoQuadraticSmoothAbs",
"SVGPathSegCurvetoQuadraticSmoothRel", "SVGPathSegLinetoAbs",
"SVGPathSegLinetoHorizontalAbs", "SVGPathSegLinetoHorizontalRel",
"SVGPathSegLinetoRel", "SVGPathSegLinetoVerticalAbs",
"SVGPathSegLinetoVerticalRel", "SVGPathSegList",
"SVGPathSegMovetoAbs", "SVGPathSegMovetoRel", "SVGPatternElement",
"SVGPoint", "SVGPointList", "SVGPolygonElement",
"SVGPolylineElement", "SVGPreserveAspectRatio",
"SVGRadialGradientElement", "SVGRect", "SVGRectElement",
"SVGRenderingIntent", "SVGSVGElement", "SVGScriptElement",
"SVGSetElement", "SVGStopElement", "SVGStringList", "SVGStylable",
"SVGStyleElement", "SVGSwitchElement", "SVGSymbolElement",
"SVGTRefElement", "SVGTSpanElement", "SVGTests",
"SVGTextContentElement", "SVGTextElement", "SVGTextPathElement",
"SVGTextPositioningElement", "SVGTitleElement", "SVGTransform",
"SVGTransformList", "SVGTransformable", "SVGURIReference",
"SVGUnitTypes", "SVGUseElement", "SVGVKernElement",
"SVGViewElement", "SVGViewSpec", "SVGZoomAndPan", "SVGZoomEvent",
"Screen", "ScriptProfile", "ScriptProfileNode", "SharedWorker",
"SharedWorkercontext", "SpeechInputEvent", "SpeechInputResult",
"SpeechInputResultList", "Storage", "StorageEvent", "StorageInfo",
"StyleMedia", "StyleSheet", "StyleSheetList", "Text", "TextEvent",
"TextMetrics", "TextTrack", "TextTrackCue", "TextTrackCueList",
"TimeRanges", "Touch", "TouchEvent", "TouchList", "TreeWalker",
"UIEvent", "Uint16Array", "Uint32Array", "Uint8Array",
"ValidityState", "VoidCallback", "WaveShaperNode",
"WebGLActiveInfo", "WebGLBuffer", "WebGLContextAttributes",
"WebGLContextEvent", "WebGLDebugRendererInfo", "WebGLDebugShaders",
"WebGLFramebuffer", "WebGLProgram", "WebGLRenderbuffer",
"WebGLRenderingContext", "WebGLShader", "WebGLTexture",
"WebGLUniformLocation", "WebGLVertexArrayObjectOES",
"WebKitAnimation", "WebKitAnimationEvent", "WebKitAnimationList",
"WebKitBlobBuilder", "WebKitCSSFilterValue",
"WebKitCSSKeyframeRule", "WebKitCSSKeyframesRule",
"WebKitCSSMatrix", "WebKitCSSTransformValue", "WebKitFlags",
"WebKitLoseContext", "WebKitMutationObserver", "WebKitPoint",
"WebKitTransitionEvent", "WebSocket", "WheelEvent", "Worker",
"WorkerContext", "WorkerLocation", "WorkerNavigator",
"XMLHttpRequest", "XMLHttpRequestException",
"XMLHttpRequestProgressEvent", "XMLHttpRequestUpload",
"XMLSerializer", "XPathEvaluator", "XPathException",
"XPathExpression", "XPathNSResolver", "XPathResult",
"XSLTProcessor", "AudioBufferCallback", "DatabaseCallback",
"EntriesCallback", "EntryCallback", "ErrorCallback", "FileCallback",
"FileSystemCallback", "FileWriterCallback", "MetadataCallback",
"NavigatorUserMediaErrorCallback", "PositionCallback",
"PositionErrorCallback", "SQLStatementCallback",
"SQLStatementErrorCallback", "SQLTransactionCallback",
"SQLTransactionErrorCallback", "SQLTransactionSyncCallback",
"StorageInfoErrorCallback", "StorageInfoQuotaCallback",
"StorageInfoUsageCallback", "StringCallback"
];
Map dbEntry;
Map get dartIdl => data['dartIdl'];
String get currentType => data['type'];
String _currentTypeShort;
String get currentTypeShort {
if (_currentTypeShort == null) {
_currentTypeShort = currentType;
_currentTypeShort = trimPrefix(_currentTypeShort, "HTML");
_currentTypeShort = trimPrefix(_currentTypeShort, "SVG");
_currentTypeShort = trimPrefix(_currentTypeShort, "DOM");
_currentTypeShort = trimPrefix(_currentTypeShort, "WebKit");
_currentTypeShort = trimPrefix(_currentTypeShort, "Webkit");
}
return _currentTypeShort;
}
String _currentTypeTiny;
String get currentTypeTiny {
if (_currentTypeTiny == null) {
_currentTypeTiny = currentTypeShort;
_currentTypeTiny = trimEnd(_currentTypeTiny, "Element");
}
return _currentTypeTiny;
}
Map get searchResult => data['searchResult'];
String get pageUrl => searchResult['link'];
String _pageDomain;
String get pageDomain {
if (_pageDomain == null) {
_pageDomain = pageUrl.substring(0, pageUrl.indexOf("/", "https://".length));
}
return _pageDomain;
}
String get pageDir {
return pageUrl.substring(0, pageUrl.lastIndexOf('/') + 1);
}
String getAbsoluteUrl(AnchorElement anchor) {
if (anchor == null || anchor.href.length == 0) return '';
String path = anchor.href;
RegExp fullUrlRegExp = new RegExp("^https?://");
if (fullUrlRegExp.hasMatch(path)) return path;
if (path.startsWith('/')) {
return "$pageDomain$path";
} else if (path.startsWith("#")) {
return "$pageUrl$path";
} else {
return "$pageDir$path";
}
}
bool inTable(Node n) {
while (n != null) {
if (n is TableElement) return true;
n = n.parent;
}
return false;
}
String escapeHTML(str) {
Element e = new Element.tag("div");
e.text = str;
return e.innerHTML;
}
List<Text> getAllTextNodes(Element elem) {
final nodes = <Text>[];
helper(Node n) {
if (n is Text) {
nodes.add(n);
} else {
for (Node child in n.nodes) {
helper(child);
}
}
};
helper(elem);
return nodes;
}
/**
* Whether a node and its children are all types that are safe to skip if the
* nodes have no text content.
*/
bool isSkippableType(Node n) {
// TODO(jacobr): are there any types we don't want to skip even if they
// have no text content?
if (n is ImageElement || n is CanvasElement || n is InputElement
|| n is ObjectElement) {
return false;
}
if (n is Text) return true;
for (final child in n.nodes) {
if (!isSkippableType(child)) {
return false;
}
}
return true;
}
bool isSkippable(Node n) {
if (!isSkippableType(n)) return false;
return n.text.trim().length == 0;
}
void onEnd() {
// Hideous hack to send JSON back to JS.
String dbJson = JSON.encode(dbEntry);
// workaround bug in JSON.decode.
dbJson = dbJson.replaceAll("ZDARTIUMDOESNTESCAPESLASHNJXXXX", "\\n");
// Use postMessage to end the JSON to JavaScript. TODO(jacobr): use a simple
// isolate based Dart-JS interop solution in the future.
window.postMessage("START_DART_MESSAGE_UNIQUE_IDENTIFIER$dbJson", "*");
}
class SectionParseResult {
final String html;
final String url;
final String idl;
SectionParseResult(this.html, this.url, this.idl);
}
String genCleanHtml(Element root) {
for (final e in root.queryAll(".$DART_REMOVED")) {
e.classes.remove(DART_REMOVED);
}
// Ditch inline styles.
for (final e in root.queryAll('[style]')) {
e.attributes.remove('style');
}
// These elements are just tags that we should suppress.
for (final e in root.queryAll(".lang.lang-en")) {
e.remove();
}
Element parametersHeader;
Element returnValueHeader;
for (final e in root.queryAll("h6")) {
if (e.text == 'Parameters') {
parametersHeader = e;
} else if (e.text == 'Return value') {
returnValueHeader = e;
}
}
if (parametersHeader != null) {
int numEmptyParameters = 0;
final parameterDescriptions = root.queryAll("dd");
for (Element parameterDescription in parameterDescriptions) {
if (parameterDescription.text.trim().length == 0) {
numEmptyParameters++;
}
}
if (numEmptyParameters > 0 &&
numEmptyParameters == parameterDescriptions.length) {
// Remove the parameter list as it adds zero value as all descriptions
// are empty.
parametersHeader.remove();
for (final e in root.queryAll("dl")) {
e.remove();
}
} else if (parameterDescriptions.length == 0 &&
parametersHeader.nextElementSibling != null &&
parametersHeader.nextElementSibling.text.trim() == 'None.') {
// No need to display that the function takes 0 parameters.
parametersHeader.nextElementSibling.remove();
parametersHeader.remove();
}
}
// Heuristic: if the return value is a single word it is a type name not a
// useful text description so suppress it.
if (returnValueHeader != null &&
returnValueHeader.nextElementSibling != null &&
returnValueHeader.nextElementSibling.text.trim().split(' ').length < 2) {
returnValueHeader.nextElementSibling.remove();
returnValueHeader.remove();
}
bool changed = true;
while (changed) {
changed = false;
while (root.nodes.length == 1 && root.nodes.first is Element) {
root = root.nodes.first;
changed = true;
}
// Trim useless nodes from the front.
while (root.nodes.length > 0 &&
isSkippable(root.nodes.first)) {
root.nodes.first.remove();
changed = true;
}
// Trim useless nodes from the back.
while (root.nodes.length > 0 &&
isSkippable(root.nodes.last)) {
root.nodes.last.remove();
changed = true;
}
}
return JSONFIXUPHACK(root.innerHTML);
}
String genPrettyHtmlFromElement(Element e) {
e = e.clone(true);
return genCleanHtml(e);
}
class PostOrderTraversalIterator implements Iterator<Node> {
Node _next;
Node _current;
PostOrderTraversalIterator(Node start) {
_next = _leftMostDescendent(start);
}
Node get current => _current;
bool get hasNext => _next != null;
bool moveNext() {
_current = _next;
if (_next == null) return false;
if (_next.nextNode != null) {
_next = _leftMostDescendent(_next.nextNode);
} else {
_next = _next.parent;
}
return true;
}
static Node _leftMostDescendent(Node n) {
while (n.nodes.length > 0) {
n = n.nodes.first;
}
return n;
}
}
class PostOrderTraversal extends IterableBase<Node> {
final Node _node;
PostOrderTraversal(this._node);
Iterator<Node> get iterator => new PostOrderTraversalIterator(_node);
}
/**
* Estimate what content represents the first line of text within the [section]
* range returning null if there isn't a plausible first line of text that
* contains the string [prop]. We measure the actual rendered client rectangle
* for the text and use heuristics defining how many pixels text can vary by
* and still be viewed as being on the same line.
*/
Range findFirstLine(Range section, String prop) {
final firstLine = newRange();
firstLine.setStart(section.startContainer, section.startOffset);
num maxBottom = null;
for (final n in new PostOrderTraversal(section.startContainer)) {
int compareResult = section.comparePoint(n, 0);
if (compareResult == -1) {
// before range so skip.
continue;
} else if (compareResult > 0) {
// After range so exit.
break;
}
final rect = getClientRect(n);
num bottom = rect.bottom;
if (rect.height > 0 && rect.width > 0) {
if (maxBottom != null &&
maxBottom + MIN_PIXELS_DIFFERENT_LINES < bottom) {
break;
} else if (maxBottom == null || maxBottom > bottom) {
maxBottom = bottom;
}
}
firstLine.setEndAfter(n);
}
// If the first line of text in the section does not contain the property
// name then we're not confident we are able to extract a high accuracy match
// so we should not return anything.
if (!firstLine.toString().contains(stripWebkit(prop))) {
return null;
}
return firstLine;
}
/** Find child anchor elements that contain the text [prop]. */
AnchorElement findAnchorElement(Element root, String prop) {
for (AnchorElement a in root.queryAll("a")) {
if (a.text.contains(prop)) {
return a;
}
}
return null;
}
// First surrounding element with an ID is safe enough.
Element findTighterRoot(Element elem, Element root) {
Element candidate = elem;
while (root != candidate) {
candidate = candidate.parent;
if (candidate.id.length > 0 && candidate.id.indexOf("section_") != 0) {
break;
}
}
return candidate;
}
// TODO(jacobr): this is very slow and ugly.. consider rewriting or at least
// commenting carefully.
SectionParseResult filteredHtml(Element elem, Element root, String prop,
Function fragmentGeneratedCallback) {
// Using a tighter root avoids false positives at the risk of trimming
// text we shouldn't.
root = findTighterRoot(elem, root);
final range = newRange();
range.setStartBefore(elem);
Element current = elem;
while (current != null) {
range.setEndBefore(current);
if (current.classes.contains(DART_REMOVED) &&
range.toString().trim().length > 0) {
break;
}
if (current.firstElementChild != null) {
current = current.firstElementChild;
} else {
while (current != null) {
range.setEndAfter(current);
if (current == root) {
current = null;
break;
}
if (current.nextElementSibling != null) {
current = current.nextElementSibling;
break;
}
current = current.parent;
}
}
}
String url = null;
if (prop != null) {
Range firstLine = findFirstLine(range, prop);
if (firstLine != null) {
range.setStart(firstLine.endContainer, firstLine.endOffset);
DocumentFragment firstLineClone = firstLine.cloneContents();
AnchorElement anchor = findAnchorElement(firstLineClone, prop);
if (anchor != null) {
url = getAbsoluteUrl(anchor);
}
}
}
final fragment = range.cloneContents();
if (fragmentGeneratedCallback != null) {
fragmentGeneratedCallback(fragment);
}
// Strip tags we don't want
for (Element e in fragment.queryAll("script, object, style")) {
e.remove();
}
// Extract idl
final idl = new StringBuffer();
if (prop != null && prop.length > 0) {
// Only expect properties to have HTML.
for(Element e in fragment.queryAll(IDL_SELECTOR)) {
idl.write(e.outerHTML);
e.remove();
}
// TODO(jacobr) this is a very basic regex to see if text looks like IDL
RegExp likelyIdl = new RegExp(" $prop\\w*\\(");
for (Element e in fragment.queryAll("pre")) {
// Check if it looks like idl...
String txt = e.text.trim();
if (likelyIdl.hasMatch(txt) && txt.contains("\n") && txt.contains(")")) {
idl.write(e.outerHTML);
e.remove();
}
}
}
return new SectionParseResult(genCleanHtml(fragment), url, idl.toString());
}
/**
* Find the best child element of [root] that appears to be an API definition
* for [prop]. [allText] is a list of all text nodes under root computed by
* the caller to improve performance.
*/
Element findBest(Element root, List<Text> allText, String prop,
String propType) {
// Best bet: find a child of root where the id matches the property name.
Element cand = root.query("#$prop");
if (cand == null && propType == "methods") {
cand = root.query("[id=$prop\\(\\)]");
}
while (cand != null && cand.text.trim().length == 0) {
// We found the bookmark for the element but sadly it is just an empty
// placeholder. Find the first real element.
cand = cand.nextElementSibling;
}
if (cand != null) {
return cand;
}
// If we are at least 70 pixels from the left, something is definitely
// fishy and we shouldn't even consider this candidate as nobody visually
// formats API docs like that.
num candLeft = 70;
for (Text text in allText) {
Element proposed = null;
// TODO(jacobr): does it hurt precision to use the full cleanup?
String t = fullNameCleanup(text.text);
if (t == prop) {
proposed = text.parent;
ClientRect candRect = getClientRect(proposed);
// TODO(jacobr): this is a good heuristic
// if (selObj.selector.indexOf(" > DD ") == -1
if (candRect.left < candLeft) {
cand = proposed;
candLeft = candRect.left;
}
}
}
return cand;
}
/**
* Checks whether [e] is tagged as obsolete or deprecated using heuristics
* for what these tags look like in the MDN docs.
*/
bool isObsolete(Element e) {
RegExp obsoleteRegExp = new RegExp(r"(^|\s)obsolete(?=\s|$)");
RegExp deprecatedRegExp = new RegExp(r"(^|\s)deprecated(?=\s|$)");
for (Element child in e.queryAll("span")) {
String t = child.text.toLowerCase();
if (t.startsWith("obsolete") || t.startsWith("deprecated")) return true;
}
String text = e.text.toLowerCase();
return obsoleteRegExp.hasMatch(text) || deprecatedRegExp.hasMatch(text);
}
bool isFirstCharLowerCase(String str) {
return new RegExp("^[a-z]").hasMatch(str);
}
/**
* Extracts information from a fragment of HTML only searching under the [root]
* html node. [secitonSelector] specifies the query to use to find candidate
* sections of the document to consider (there may be more than one).
* [currentType] specifies the name of the current class. [members] specifies
* the known class members for this class that we are attempting to find
* documentation for. [propType] indicates whether we are searching for
* methods, properties, constants, or constructors.
*/
void scrapeSection(Element root, String sectionSelector, String currentType,
List members, String propType) {
Map expectedProps = dartIdl[propType];
Set<String> alreadyMatchedProperties = new Set<String>();
bool onlyConsiderTables = false;
ElementList allMatches = root.queryAll(sectionSelector);
if (allMatches.length == 0) {
// If we can't find any matches to the sectionSelector, we fall back to
// considering all tables in the document. This is dangerous so we only
// allow the safer table matching extraction rules for this case.
allMatches = root.queryAll(".fullwidth-table");
onlyConsiderTables = true;
}
for (Element matchElement in allMatches) {
final match = matchElement.parent;
if (!match.id.startsWith("section") && match.id != "pageText") {
throw "Unexpected element $match";
}
// We don't want to later display this text a second time while for example
// displaying class level summary information as then we would display
// the same documentation twice.
match.classes.add(DART_REMOVED);
bool foundProps = false;
// TODO(jacobr): we should really look for the table tag instead
// add an assert if we are missing something that is a table...
// TODO(jacobr) ignore tables in tables.
for (Element t in match.queryAll('.standard-table, .fullwidth-table')) {
int helpIndex = -1;
num i = 0;
for (Element r in t.queryAll("th, td.header")) {
final txt = r.text.trim().split(" ")[0].toLowerCase();
if (txt == "description") {
helpIndex = i;
break;
}
i++;
}
// Figure out which column in the table contains member names by
// tracking how many member names each column contains.
final numMatches = new List<int>(i);
for (int j = 0; j < i; j++) {
numMatches[j] = 0;
}
// Find the column that seems to have the most names that look like
// expected properties.
for (Element r in t.queryAll("tbody tr")) {
ElementList row = r.elements;
if (row.length == 0 || row.first.classes.contains(".header")) {
continue;
}
for (int k = 0; k < numMatches.length && k < row.length; k++) {
if (expectedProps.containsKey(fullNameCleanup(row[k].text))) {
numMatches[k]++;
break;
}
}
}
int propNameIndex = 0;
{
int bestCount = numMatches[0];
for (int k = 1; k < numMatches.length; k++) {
if (numMatches[k] > bestCount) {
bestCount = numMatches[k];
propNameIndex = k;
}
}
}
for (Element r in t.queryAll("tbody tr")) {
final row = r.elements;
if (row.length > propNameIndex && row.length > helpIndex) {
if (row.first.classes.contains(".header")) {
continue;
}
// TODO(jacobr): this code for determining the namestr is needlessly
// messy.
final nameRow = row[propNameIndex];
AnchorElement a = nameRow.query("a");
String goodName = '';
if (a != null) {
goodName = a.text.trim();
}
String nameStr = nameRow.text;
Map entry = new Map<String, String>();
entry["name"] = fullNameCleanup(nameStr.length > 0 ?
nameStr : goodName);
final parse = filteredHtml(nameRow, nameRow, entry["name"], null);
String altHelp = parse.html;
entry["help"] = (helpIndex == -1 || row[helpIndex] == null) ?
altHelp : genPrettyHtmlFromElement(row[helpIndex]);
if (parse.url != null) {
entry["url"] = parse.url;
}
if (parse.idl.length > 0) {
entry["idl"] = parse.idl;
}
entry["obsolete"] = isObsolete(r);
if (entry["name"].length > 0) {
cleanupEntry(members, entry);
alreadyMatchedProperties.add(entry['name']);
foundProps = true;
}
}
}
}
if (onlyConsiderTables) {
continue;
}
// After this point we have higher risk tests that attempt to perform
// rudimentary page segmentation. This approach is much more error-prone
// than using tables because the HTML is far less clearly structured.
final allText = getAllTextNodes(match);
final pmap = new Map<String, Element>();
for (final prop in expectedProps.keys) {
if (alreadyMatchedProperties.contains(prop)) {
continue;
}
final e = findBest(match, allText, prop, propType);
if (e != null && !inTable(e)) {
pmap[prop] = e;
}
}
for (final prop in pmap.keys) {
pmap[prop].classes.add(DART_REMOVED);
}
// The problem is the MDN docs do place documentation for each method in a
// nice self contained subtree. Instead you will see something like:
// <h3>drawImage</h3>
// <p>Draw image is an awesome method</p>
// some more info on drawImage here
// <h3>mozDrawWindow</h3>
// <p>This API cannot currently be used by Web content.
// It is chrome only.</p>
// <h3>drawRect</h3>
// <p>Always call drawRect instead of drawImage</p>
// some more info on drawRect here...
// The trouble is we will easily detect that the drawImage and drawRect
// entries are method definitions because we know to search for these
// method names but we will not detect that mozDrawWindow is a method
// definition as that method doesn't exist in our IDL. Thus if we are not
// careful the definition for the drawImage method will contain the
// definition for the mozDrawWindow method as well which would result in
// broken docs. We solve this problem by finding all content with similar
// visual structure to the already found method definitions. It turns out
// that using the visual position of each element on the page is much
// more reliable than using the DOM structure
// (e.g. section_root > div > h3) for the MDN docs because MDN authors
// carefully check that the documentation for each method comment is
// visually consistent but take less care to check that each
// method comment has identical markup structure.
for (String prop in pmap.keys) {
Element e = pmap[prop];
ClientRect r = getClientRect(e);
// TODO(jacobr): a lot of these queries are identical and this code
// could easily be optimized.
for (final cand in match.queryAll(e.tagName)) {
// TODO(jacobr): use a negative selector instead.
if (!cand.classes.contains(DART_REMOVED) && !inTable(cand)) {
final candRect = getClientRect(cand);
// Only consider matches that have similar heights and identical left
// coordinates.
if (candRect.left == r.left &&
(candRect.height - r.height).abs() < 5) {
String propName = fullNameCleanup(cand.text);
if (isFirstCharLowerCase(propName) && !pmap.containsKey(propName)
&& !alreadyMatchedProperties.contains(propName)) {
pmap[propName] = cand;
}
}
}
}
}
// We mark these elements in batch to reduce the number of layouts
// triggered. TODO(jacobr): use new batch based async measurement to make
// this code flow simpler.
for (String prop in pmap.keys) {
Element e = pmap[prop];
e.classes.add(DART_REMOVED);
}
// Find likely "subsections" of the main section and mark them with
// DART_REMOVED so we don't include them in member descriptions... which
// would suck.
for (Element e in match.queryAll("[id]")) {
if (e.id.contains(matchElement.id)) {
e.classes.add(DART_REMOVED);
}
}
for (String prop in pmap.keys) {
Element elem = pmap[prop];
bool obsolete = false;
final parse = filteredHtml(
elem, match, prop,
(Element e) {
obsolete = isObsolete(e);
});
Map entry = {
"url" : parse.url,
"name" : prop,
"help" : parse.html,
"obsolete" : obsolete
};
if (parse.idl.length > 0) {
entry["idl"] = parse.idl;
}
cleanupEntry(members, entry);
}
}
}
String trimHtml(String html) {
// TODO(jacobr): implement this. Remove spurious enclosing HTML tags, etc.
return html;
}
bool maybeName(String name) {
return new RegExp("^[a-z][a-z0-9A-Z]+\$").hasMatch(name) ||
new RegExp("^[A-Z][A-Z_]*\$").hasMatch(name);
}
// TODO(jacobr): this element is ugly at the moment but will become easier to
// read once ElementList supports most of the Element functionality.
void markRemoved(var e) {
if (e != null) {
if (e is Element) {
e.classes.add(DART_REMOVED);
} else {
for (Element el in e) {
el.classes.add(DART_REMOVED);
}
}
}
}
// TODO(jacobr): remove this when the dartium JSON parse handles \n correctly.
String JSONFIXUPHACK(String value) {
return value.replaceAll("\n", "ZDARTIUMDOESNTESCAPESLASHNJXXXX");
}
String mozToWebkit(String name) {
return name.replaceFirst(new RegExp("^moz"), "webkit");
}
String stripWebkit(String name) {
return trimPrefix(name, "webkit");
}
// TODO(jacobr): be more principled about this.
String fullNameCleanup(String name) {
int parenIndex = name.indexOf('(');
if (parenIndex != -1) {
name = name.substring(0, parenIndex);
}
name = name.split(" ")[0];
name = name.split("\n")[0];
name = name.split("\t")[0];
name = name.split("*")[0];
name = name.trim();
name = safeNameCleanup(name);
return name;
}
// Less agressive than the full name cleanup to avoid overeager matching.
// TODO(jacobr): be more principled about this.
String safeNameCleanup(String name) {
int parenIndex = name.indexOf('(');
if (parenIndex != -1 && name.indexOf(")") != -1) {
// TODO(jacobr): workaround bug in:
// name = name.split("(")[0];
name = name.substring(0, parenIndex);
}
name = name.trim();
name = trimPrefix(name, currentType + ".");
name = trimPrefix(name, currentType.toLowerCase() + ".");
name = trimPrefix(name, currentTypeShort + ".");
name = trimPrefix(name, currentTypeShort.toLowerCase() + ".");
name = trimPrefix(name, currentTypeTiny + ".");
name = trimPrefix(name, currentTypeTiny.toLowerCase() + ".");
name = name.trim();
name = mozToWebkit(name);
return name;
}
/**
* Remove h1, h2, and h3 headers.
*/
void removeHeaders(DocumentFragment fragment) {
for (Element e in fragment.queryAll("h1, h2, h3")) {
e.remove();
}
}
/**
* Given an [entry] representing a single method or property cleanup the
* values performing some simple normalization and only adding the entry to
* [members] if it has a valid name.
*/
void cleanupEntry(List members, Map entry) {
if (entry.containsKey('help')) {
entry['help'] = trimHtml(entry['help']);
}
String name = fullNameCleanup(entry['name']);
entry['name'] = name;
if (maybeName(name)) {
for (String key in entry.keys) {
var value = entry[key];
if (value == null) {
entry.remove(key);
continue;
}
if (value is String) {
entry[key] = JSONFIXUPHACK(value);
}
}
members.add(entry);
}
}
// TODO(jacobr) dup with trim start....
String trimPrefix(String str, String prefix) {
if (str.indexOf(prefix) == 0) {
return str.substring(prefix.length);
} else {
return str;
}
}
String trimStart(String str, String start) {
if (str.startsWith(start) && str.length > start.length) {
return str.substring(start.length);
}
return str;
}
String trimEnd(String str, String end) {
if (str.endsWith(end) && str.length > end.length) {
return str.substring(0, str.length - end.length);
}
return str;
}
/**
* Extract a section with name [key] using [selector] to find start points for
* the section in the document.
*/
void extractSection(String selector, String key) {
for (Element e in document.queryAll(selector)) {
e = e.parent;
for (Element skip in e.queryAll("h1, h2, $IDL_SELECTOR")) {
skip.remove();
}
String html = filteredHtml(e, e, null, removeHeaders).html;
if (html.length > 0) {
if (dbEntry.containsKey(key)) {
dbEntry[key] += html;
} else {
dbEntry[key] = html;
}
}
e.classes.add(DART_REMOVED);
}
}
void run() {
// Inject CSS to ensure lines don't wrap unless they were intended to.
// This is needed to make the logic to determine what is a single line
// behave consistently even for very long method names.
document.head.nodes.add(new Element.html("""
<style type="text/css">
body {
width: 10000px;
}
</style>"""));
String title = trimEnd(window.document.title.trim(), " - MDN");
dbEntry['title'] = title;
// TODO(rnystrom): Clean up the page a bunch. Not sure if this is the best
// place to do this...
// TODO(jacobr): move this to right before we extract HTML.
// Remove the "Introduced in HTML <version>" boxes.
for (Element e in document.queryAll('.htmlVersionHeaderTemplate')) {
e.remove();
}
// Flatten the list of known DOM types into a faster and case-insensitive
// map.
domTypes = {};
for (final domType in domTypesRaw) {
domTypes[domType.toLowerCase()] = domType;
}
// Fix up links.
final SHORT_LINK = new RegExp(r'^[\w/]+$');
final INNER_LINK = new RegExp(r'[Ee]n/(?:[\w/]+/|)([\w#.]+)(?:\(\))?$');
final MEMBER_LINK = new RegExp(r'(\w+)[.#](\w+)');
final RELATIVE_LINK = new RegExp(r'^(?:../)*/?[Ee][Nn]/(.+)');
// - Make relative links absolute.
// - If we can, take links that point to other MDN pages and retarget them
// to appropriate pages in our docs.
// TODO(rnystrom): Add rel external to links we didn't fix.
for (AnchorElement a in document.queryAll('a')) {
// Get the raw attribute because we *don't* want the browser to fully-
// qualify the name for us since it has the wrong base address for the
// page.
var href = a.attributes['href'];
// Ignore busted links.
if (href == null) continue;
// If we can recognize what it's pointing to, point it to our page instead.
tryToLinkToRealType(maybeType) {
// See if we know a type with that name.
final realType = domTypes[maybeType.toLowerCase()];
if (realType != null) {
href = '../html/$realType.html';
}
}
// If it's a relative link (that we know how to root), make it absolute.
var match = RELATIVE_LINK.firstMatch(href);
if (match != null) {
href = 'https://developer.mozilla.org/en/${match[1]}';
}
// If it's a word link like "foo" find a type or make it absolute.
match = SHORT_LINK.firstMatch(href);
if (match != null) {
href = 'https://developer.mozilla.org/en/DOM/${match[0]}';
}
// TODO(rnystrom): This is a terrible way to do this. Should use the real
// mapping from DOM names to html class names that we use elsewhere in the
// DOM scripts.
match = INNER_LINK.firstMatch(href);
if (match != null) {
// See if we're linking to a member ("type.name" or "type#name") or just
// a type ("type").
final member = MEMBER_LINK.firstMatch(match[1]);
if (member != null) {
tryToLinkToRealType(member[1]);
} else {
tryToLinkToRealType(match[1]);
}
}
// Put it back into the element.
a.attributes['href'] = href;
}
if (!title.toLowerCase().contains(currentTypeTiny.toLowerCase())) {
bool foundMatch = false;
// Test out if the title is really an HTML tag that matches the
// current class name.
for (String tag in [title.split(" ")[0], title.split(".").last]) {
try {
Element element = new Element.tag(tag);
// TODO(jacobr): this is a really ugly way of doing this that will
// stop working at some point soon.
if (element.typeName == currentType) {
foundMatch = true;
break;
}
} catch (e) {}
}
if (!foundMatch) {
dbEntry['skipped'] = true;
dbEntry['cause'] = "Suspect title";
onEnd();
return;
}
}
Element root = document.query(".pageText");
if (root == null) {
dbEntry['cause'] = '.pageText not found';
onEnd();
return;
}
markRemoved(root.query("#Notes"));
List members = dbEntry['members'];
// This is a laundry list of CSS selectors for boilerplate content on the
// MDN pages that we should ignore for the purposes of extracting
// documentation.
markRemoved(document.queryAll(".pageToc, footer, header, #nav-toolbar"));
markRemoved(document.queryAll("#article-nav"));
markRemoved(document.queryAll(".hideforedit"));
markRemoved(document.queryAll(".navbox"));
markRemoved(document.query("#Method_overview"));
markRemoved(document.queryAll("h1, h2"));
scrapeSection(root, "#Methods", currentType, members, 'methods');
scrapeSection(root, "#Constants, #Error_codes, #State_constants",
currentType, members, 'constants');
// TODO(jacobr): infer tables based on multiple matches rather than
// using a hard coded list of section ids.
scrapeSection(root,
"[id^=Properties], #Notes, [id^=Other_properties], #Attributes, " +
"#DOM_properties, #Event_handlers, #Event_Handlers",
currentType, members, 'properties');
// Avoid doing this till now to avoid messing up the section scrape.
markRemoved(document.queryAll("h3"));
ElementList examples = root.queryAll("span[id^=example], span[id^=Example]");
extractSection("#See_also", 'seeAlso');
extractSection("#Specification, #Specifications", "specification");
// TODO(jacobr): actually extract the constructor(s)
extractSection("#Constructor, #Constructors", 'constructor');
extractSection("#Browser_compatibility, #Compatibility", 'compatibility');
// Extract examples.
List<String> exampleHtml = [];
for (Element e in examples) {
e.classes.add(DART_REMOVED);
}
for (Element e in examples) {
String html = filteredHtml(e, root, null,
(DocumentFragment fragment) {
removeHeaders(fragment);
if (fragment.text.trim().toLowerCase() == "example") {
// Degenerate example.
fragment.nodes.clear();
}
}).html;
if (html.length > 0) {
exampleHtml.add(html);
}
}
if (exampleHtml.length > 0) {
dbEntry['examples'] = exampleHtml;
}
// Extract the class summary.
// Basically everything left over after the #Summary or #Description tag is
// safe to include in the summary.
StringBuffer summary = new StringBuffer();
for (Element e in root.queryAll("#Summary, #Description")) {
summary.write(filteredHtml(root, e, null, removeHeaders).html);
}
if (summary.length == 0) {
// Remove the "Gecko DOM Reference text"
Element ref = root.query(".lang.lang-en");
if (ref != null) {
ref = ref.parent;
String refText = ref.text.trim();
if (refText == "Gecko DOM Reference" ||
refText == "« Gecko DOM Reference") {
ref.remove();
}
}
// Risky... this might add stuff we shouldn't.
summary.write(filteredHtml(root, root, null, removeHeaders).html);
}
if (summary.length > 0) {
dbEntry['summary'] = summary.toString();
}
// Inject CSS to aid debugging in the browser.
// We could avoid doing this if we know we are not running in a browser..
document.head.nodes.add(new Element.html(DEBUG_CSS));
onEnd();
}
void main() {
window.on.load.add(documentLoaded);
}
void documentLoaded(event) {
// Load the database of expected methods and properties with an HttpRequest.
new HttpRequest.get('${window.location}.json', (req) {
data = JSON.decode(req.responseText);
dbEntry = {'members': [], 'srcUrl': pageUrl};
run();
});
}