1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @constructor
7 * @extends {WebInspector.View}
8 */
9WebInspector.DocumentationView = function()
10{
11    WebInspector.View.call(this);
12    this.element.classList.add("documentation-view");
13    this.registerRequiredCSS("documentationView.css");
14}
15
16/**
17 * @param {string} url
18 * @param {string} searchItem
19 */
20WebInspector.DocumentationView.showDocumentationURL = function(url, searchItem)
21{
22    if (!WebInspector.DocumentationView._view)
23        WebInspector.DocumentationView._view = new WebInspector.DocumentationView();
24    var view = WebInspector.DocumentationView._view;
25    view.element.removeChildren();
26    WebInspector.inspectorView.showCloseableViewInDrawer("documentation", WebInspector.UIString("Documentation"), view);
27    view.showDocumentation(url, searchItem);
28}
29
30WebInspector.DocumentationView._languageToMimeType = {
31    "javascript": "text/javascript",
32    "html": "text/html"
33};
34
35WebInspector.DocumentationView.prototype = {
36    /**
37     * @param {string} url
38     * @param {string} searchItem
39     */
40    showDocumentation: function(url, searchItem)
41    {
42        if (!url) {
43            this._createEmptyPage();
44            return;
45        }
46        loadXHR(url)
47            .then(this._createArticle.bind(this, searchItem), this._createEmptyPage.bind(this))
48            .catch(this._createEmptyPage.bind(this));
49    },
50
51    /**
52     * @param {string} searchItem
53     * @param {string} responseText
54     */
55    _createArticle: function(searchItem, responseText)
56    {
57        var json = JSON.parse(responseText);
58        var pages = json["query"]["pages"];
59        var wikiKeys = Object.keys(pages);
60        if (wikiKeys.length === 1 && wikiKeys[0] === "-1") {
61            this._createEmptyPage();
62            return;
63        }
64        var wikiMarkupText = pages[wikiKeys[0]]["revisions"]["0"]["*"];
65        var article;
66        try {
67            article = WebInspector.JSArticle.parse(wikiMarkupText);
68        } catch (error) {
69            console.error("Article could not be parsed. " + error.message);
70        }
71        if (!article) {
72            this._createEmptyPage();
73            return;
74        }
75
76        this.element.removeChildren();
77        var renderer = new WebInspector.DocumentationView.Renderer(article, searchItem);
78        this.element.appendChild(renderer.renderJSArticle());
79    },
80
81    _createEmptyPage: function()
82    {
83        this.element.removeChildren();
84        var emptyPage = this.element.createChild("div", "documentation-empty-page fill");
85        var pageTitle = emptyPage.createChild("div", "documentation-not-found");
86        pageTitle.textContent = WebInspector.UIString("No documentation found.");
87        emptyPage.createChild("div", "documentation-empty-page-align");
88    },
89
90    __proto__: WebInspector.View.prototype
91}
92
93/**
94 * @constructor
95 * @param {!WebInspector.JSArticle} article
96 * @param {string} searchItem
97 */
98WebInspector.DocumentationView.Renderer = function(article, searchItem)
99{
100    this._searchItem = searchItem;
101    this._element = document.createElement("div");
102    this._article = article;
103}
104
105WebInspector.DocumentationView.Renderer.prototype = {
106    /**
107     * @return {!Element}
108     */
109    renderJSArticle: function()
110    {
111        this._element.appendChild(this._createPageTitle(this._article.pageTitle, this._searchItem));
112        var signatureElement = this._createSignatureSection(this._article.parameters, this._article.methods);
113        if (signatureElement)
114            this._element.appendChild(signatureElement);
115
116        var descriptionElement = this._element.createChild("div", "documentation-description");
117        var summarySection = this._article.summary ? this._renderBlock(this._article.summary) : null;
118        if (summarySection)
119            descriptionElement.appendChild(summarySection);
120        var parametersSection = this._createParametersSection(this._article.parameters);
121        if (parametersSection)
122            descriptionElement.appendChild(parametersSection);
123
124        var examplesSection = this._createExamplesSection(this._article.examples);
125        if (examplesSection) {
126            var examplesTitle = this._element.createChild("div", "documentation-title");
127            examplesTitle.textContent = WebInspector.UIString("Examples");
128            descriptionElement = this._element.createChild("div", "documentation-description");
129            descriptionElement.appendChild(examplesSection);
130        }
131
132        var remarksSection = this._article.remarks ? this._renderBlock(this._article.remarks) : null;
133        if (remarksSection) {
134            var remarksTitle = this._element.createChild("div", "documentation-title");
135            remarksTitle.textContent = WebInspector.UIString("Remarks");
136            descriptionElement = this._element.createChild("div", "documentation-description");
137            descriptionElement.appendChild(remarksSection);
138        }
139        return this._element;
140    },
141
142    /**
143     * @param {string} titleText
144     * @param {string} searchItem
145     * @return {!Element}
146     */
147    _createPageTitle: function(titleText, searchItem)
148    {
149        var pageTitle = document.createElementWithClass("div", "documentation-page-title");
150        if (titleText)
151            pageTitle.textContent = titleText;
152        else if (searchItem)
153            pageTitle.textContent = searchItem;
154        return pageTitle;
155    },
156
157    /**
158     * @param {!Array.<!WebInspector.JSArticle.Parameter>} parameters
159     * @param {?WebInspector.JSArticle.Method} method
160     * @return {?Element}
161     */
162    _createSignatureSection: function(parameters, method)
163    {
164        if (!parameters.length && !method)
165            return null;
166        var signature = document.createElementWithClass("div", "documentation-method-signature monospace");
167        if (method && method.returnValueName) {
168            var returnTypeElement = signature.createChild("span", "documentation-parameter-data-type-value");
169            returnTypeElement.textContent = method.returnValueName;
170        }
171        var methodName = signature.createChild("span", "documentation-method-name");
172        methodName.textContent = this._searchItem.split(".").peekLast() + "(";
173        for (var i = 0; i < parameters.length; ++i) {
174            if (i > 0)
175                signature.createTextChild(",")
176            var parameterType = signature.createChild("span", "documentation-parameter-data-type-value");
177            parameterType.textContent = parameters[i].dataType;
178            var parameterName = signature.createChild("span", "documentation-parameter-name");
179            parameterName.textContent = parameters[i].name;
180        }
181
182        signature.createTextChild(")");
183        return signature;
184    },
185
186    /**
187     * @param {!Array.<!WebInspector.JSArticle.Parameter>} parameters
188     * @return {?Element}
189     */
190    _createParametersSection: function(parameters)
191    {
192        if (!parameters.length)
193            return null;
194        var table = document.createElementWithClass("table", "documentation-table");
195        var tableBody = table.createChild("tbody");
196        var headerRow = tableBody.createChild("tr", "documentation-table-row");
197        var tableHeader = headerRow.createChild("th", "documentation-table-header");
198        tableHeader.textContent = WebInspector.UIString("Parameters");
199        tableHeader.colSpan = 3;
200        for (var i = 0; i < parameters.length; ++i) {
201            var tableRow = tableBody.createChild("tr", "documentation-table-row");
202            var type = tableRow.createChild("td", "documentation-table-cell");
203            type.textContent = parameters[i].dataType;
204            var name = tableRow.createChild("td", "documentation-table-cell");
205            name.textContent = parameters[i].optional ? WebInspector.UIString("(optional)\n") : "";
206            name.textContent += parameters[i].name;
207            var description = tableRow.createChild("td", "documentation-table-cell");
208            if (parameters[i].description)
209                description.appendChild(this._renderBlock(/** @type {!WebInspector.WikiParser.Block} */(parameters[i].description)));
210        }
211        return table;
212    },
213
214    /**
215     * @param {!Array.<!WebInspector.JSArticle.Example>} examples
216     */
217    _createExamplesSection: function(examples)
218    {
219        if (!examples.length)
220            return;
221
222        var section = document.createElementWithClass("div", "documentation-section");
223
224        for (var i = 0; i < examples.length; ++i) {
225            var example = section.createChild("div", "documentation-example");
226            var exampleDescription = example.createChild("div", "documentation-example-description-section");
227            if (examples[i].description) {
228                var description = this._renderBlock(/** @type {!WebInspector.WikiParser.Block} */(examples[i].description));
229                description.classList.add("documentation-text");
230                exampleDescription.appendChild(description);
231            }
232            var code = example.createChild("div", "documentation-code source-code");
233            code.textContent = examples[i].code;
234            if (!examples[i].language)
235                continue;
236            var syntaxHighlighter = new WebInspector.DOMSyntaxHighlighter(WebInspector.DocumentationView._languageToMimeType[examples[i].language.toLowerCase()], true);
237            syntaxHighlighter.syntaxHighlightNode(code);
238        }
239        return section;
240    },
241
242    /**
243     * @param {!WebInspector.WikiParser.ArticleElement} article
244     * @return {!Element}
245     */
246    _renderBlock: function(article)
247    {
248        var element;
249        var elementTypes = WebInspector.WikiParser.ArticleElement.Type;
250
251        switch (article.type()) {
252        case elementTypes.Inline:
253            element = document.createElement("span");
254            break;
255        case elementTypes.Link:
256            element = document.createElementWithClass("a", "documentation-link");
257            element.href = article.url();
258            if (!article.children().length)
259                element.textContent = article.url();
260            break;
261        case elementTypes.Code:
262            element = document.createElementWithClass("span", "documentation-code-tag");
263            break;
264        case elementTypes.CodeBlock:
265            element = document.createElementWithClass("pre", "documentation-code source-code");
266            element.textContent = article.code();
267            break;
268        case elementTypes.PlainText:
269            element = document.createElement("span");
270            element.textContent = article.text();
271            if (article.isHighlighted())
272                element.classList.add("documentation-highlighted-text");
273            break;
274        case elementTypes.Block:
275            element = document.createElement(article.hasBullet() ? "li" : "div");
276            if (!article.hasBullet())
277                element.classList.add("documentation-paragraph");
278            break;
279        case elementTypes.Table:
280            return this._renderTable(/** @type {!WebInspector.WikiParser.Table} */(article));
281        default:
282            throw new Error("Unknown ArticleElement type " + article.type());
283        }
284
285        if (article.type() === WebInspector.WikiParser.ArticleElement.Type.Block
286            || article.type() === WebInspector.WikiParser.ArticleElement.Type.Code
287            || article.type() === WebInspector.WikiParser.ArticleElement.Type.Inline) {
288            for (var i = 0; i < article.children().length; ++i) {
289                var child = this._renderBlock(article.children()[i]);
290                if (child)
291                    element.appendChild(child);
292            }
293        }
294
295        return element;
296    },
297
298    /**
299     * @param {!WebInspector.WikiParser.Table} table
300     * @return {!Element}
301     */
302    _renderTable: function(table)
303    {
304        var tableElement = document.createElementWithClass("table", "documentation-table");
305        var tableBody = tableElement.createChild("tbody");
306        var headerRow = tableBody.createChild("tr", "documentation-table-row");
307        for (var i = 0; i < table.columnNames().length; ++i) {
308            var tableHeader = headerRow.createChild("th", "documentation-table-header");
309            tableHeader.appendChild(this._renderBlock(table.columnNames()[i]));
310        }
311        for (var i = 0; i < table.rows().length; ++i) {
312            var tableRow = tableBody.createChild("tr", "documentation-table-row");
313            var row = table.rows()[i];
314            for (var j = 0; j < row.length; ++j) {
315                var cell = tableRow.createChild("td", "documentation-table-cell");
316                cell.appendChild(this._renderBlock(row[j]));
317            }
318        }
319
320        return tableElement;
321    }
322}
323
324/**
325 * @constructor
326 * @implements {WebInspector.ContextMenu.Provider}
327 */
328WebInspector.DocumentationView.ContextMenuProvider = function()
329{
330}
331
332WebInspector.DocumentationView.ContextMenuProvider.prototype = {
333    /**
334     * @param {!Event} event
335     * @param {!WebInspector.ContextMenu} contextMenu
336     * @param {!Object} target
337     */
338    appendApplicableItems: function(event, contextMenu, target)
339    {
340        if (!(target instanceof WebInspector.CodeMirrorTextEditor))
341            return;
342        WebInspector.DocumentationCatalog.instance().startLoadingIfNeeded();
343        if (WebInspector.DocumentationCatalog.instance().isLoading()) {
344            var itemName = WebInspector.useLowerCaseMenuTitles() ? "Loading documentation..." : "Loading Documentation...";
345            contextMenu.appendItem(itemName, function() {}, true);
346            return;
347        }
348        var textEditor = /** @type {!WebInspector.CodeMirrorTextEditor} */ (target);
349        var descriptors = this._determineDescriptors(textEditor);
350        if (!descriptors.length)
351            return;
352        if (descriptors.length === 1) {
353            var formatString = WebInspector.useLowerCaseMenuTitles() ? "Show documentation for %s.%s" : "Show Documentation for %s.%s";
354            var methodName = String.sprintf("%s.%s", descriptors[0].name(), descriptors[0].searchItem());
355            contextMenu.appendItem(WebInspector.UIString(formatString, descriptors[0].name(), descriptors[0].searchItem()), WebInspector.DocumentationView.showDocumentationURL.bind(null, descriptors[0].url(), methodName));
356            return;
357        }
358        var subMenuItem = contextMenu.appendSubMenuItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Show documentation for..." : "Show Documentation for..."));
359        for (var i = 0; i < descriptors.length; ++i) {
360            var methodName = String.sprintf("%s.%s", descriptors[i].name(), descriptors[i].searchItem());
361            subMenuItem.appendItem(methodName, WebInspector.DocumentationView.showDocumentationURL.bind(null, descriptors[i].url(), methodName));
362        }
363    },
364
365    /**
366     * @param {!WebInspector.CodeMirrorTextEditor} textEditor
367     * @return {!Array.<!WebInspector.DocumentationCatalog.ItemDescriptor>}
368     */
369    _determineDescriptors: function(textEditor)
370    {
371        var catalog = WebInspector.DocumentationCatalog.instance();
372        var textSelection = textEditor.selection().normalize();
373        var previousTokenText = findPreviousToken(textSelection);
374
375        if (!textSelection.isEmpty()) {
376            if (textSelection.startLine !== textSelection.endLine)
377                return [];
378            return computeDescriptors(textSelection);
379        }
380
381        var descriptors = computeDescriptors(getTokenRangeByColumn(textSelection.startColumn));
382        if (descriptors.length)
383            return descriptors;
384
385        return computeDescriptors(getTokenRangeByColumn(textSelection.startColumn - 1));
386
387        /**
388         * @param {number} column
389         * @return {?WebInspector.TextRange}
390         */
391        function getTokenRangeByColumn(column)
392        {
393            var token = textEditor.tokenAtTextPosition(textSelection.startLine, column);
394            if (!token)
395                return null;
396            return new WebInspector.TextRange(textSelection.startLine, token.startColumn, textSelection.startLine, token.endColumn);
397        }
398
399        /**
400         * @param {?WebInspector.TextRange} textRange
401         * @return {!Array.<!WebInspector.DocumentationCatalog.ItemDescriptor>}
402         */
403        function computeDescriptors(textRange)
404        {
405            if (!textRange)
406                return [];
407            var propertyName = textEditor.copyRange(textRange);
408            var descriptors = catalog.itemDescriptors(propertyName);
409            if (descriptors.length)
410                return descriptors;
411            if (propertyName.toUpperCase() !== propertyName || !previousTokenText || !window[previousTokenText] || !window[previousTokenText][propertyName])
412                return [];
413            return catalog.constantDescriptors(previousTokenText);
414        }
415
416        /**
417         * @param {!WebInspector.TextRange} textRange
418         * @return {?string}
419         */
420        function findPreviousToken(textRange)
421        {
422            var line = textEditor.line(textRange.startLine);
423            if (textRange.startColumn < 3 || line[textRange.startColumn - 1] !== ".")
424                return null;
425            var token = textEditor.tokenAtTextPosition(textRange.startLine, textRange.startColumn - 2);
426            return token ? line.substring(token.startColumn, token.endColumn) : null;
427        }
428    }
429}