blob: d773192e55a26b49db1088df499b54b8f6df5e23 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @implements {UI.Searchable}
* @unrestricted
*/
export class XMLView extends UI.Widget {
/**
* @param {!Document} parsedXML
*/
constructor(parsedXML) {
super(true);
this.registerRequiredCSS('source_frame/xmlView.css');
this.contentElement.classList.add('shadow-xml-view', 'source-code');
this._treeOutline = new UI.TreeOutlineInShadow();
this._treeOutline.registerRequiredCSS('source_frame/xmlTree.css');
this.contentElement.appendChild(this._treeOutline.element);
/** @type {?UI.SearchableView} */
this._searchableView;
/** @type {number} */
this._currentSearchFocusIndex = 0;
/** @type {!Array.<!UI.TreeElement>} */
this._currentSearchTreeElements = [];
/** @type {?UI.SearchableView.SearchConfig} */
this._searchConfig;
XMLViewNode.populate(this._treeOutline, parsedXML, this);
this._treeOutline.firstChild().select(true /* omitFocus */, false /* selectedByUser */);
}
/**
* @param {!Document} parsedXML
* @return {!UI.SearchableView}
*/
static createSearchableView(parsedXML) {
const xmlView = new XMLView(parsedXML);
const searchableView = new UI.SearchableView(xmlView);
searchableView.setPlaceholder(Common.UIString('Find'));
xmlView._searchableView = searchableView;
xmlView.show(searchableView.element);
return searchableView;
}
/**
* @param {string} text
* @param {string} mimeType
* @return {?Document}
*/
static parseXML(text, mimeType) {
let parsedXML;
try {
parsedXML = (new DOMParser()).parseFromString(text, mimeType);
} catch (e) {
return null;
}
if (parsedXML.body) {
return null;
}
return parsedXML;
}
/**
* @param {number} index
* @param {boolean} shouldJump
*/
_jumpToMatch(index, shouldJump) {
if (!this._searchConfig) {
return;
}
const regex = this._searchConfig.toSearchRegex(true);
const previousFocusElement = this._currentSearchTreeElements[this._currentSearchFocusIndex];
if (previousFocusElement) {
previousFocusElement.setSearchRegex(regex);
}
const newFocusElement = this._currentSearchTreeElements[index];
if (newFocusElement) {
this._updateSearchIndex(index);
if (shouldJump) {
newFocusElement.reveal(true);
}
newFocusElement.setSearchRegex(regex, UI.highlightedCurrentSearchResultClassName);
} else {
this._updateSearchIndex(0);
}
}
/**
* @param {number} count
*/
_updateSearchCount(count) {
if (!this._searchableView) {
return;
}
this._searchableView.updateSearchMatchesCount(count);
}
/**
* @param {number} index
*/
_updateSearchIndex(index) {
this._currentSearchFocusIndex = index;
if (!this._searchableView) {
return;
}
this._searchableView.updateCurrentMatchIndex(index);
}
/**
* @param {boolean} shouldJump
* @param {boolean=} jumpBackwards
*/
_innerPerformSearch(shouldJump, jumpBackwards) {
if (!this._searchConfig) {
return;
}
let newIndex = this._currentSearchFocusIndex;
const previousSearchFocusElement = this._currentSearchTreeElements[newIndex];
this._innerSearchCanceled();
this._currentSearchTreeElements = [];
const regex = this._searchConfig.toSearchRegex(true);
for (let element = this._treeOutline.rootElement(); element; element = element.traverseNextTreeElement(false)) {
if (!(element instanceof XMLViewNode)) {
continue;
}
const hasMatch = element.setSearchRegex(regex);
if (hasMatch) {
this._currentSearchTreeElements.push(element);
}
if (previousSearchFocusElement === element) {
const currentIndex = this._currentSearchTreeElements.length - 1;
if (hasMatch || jumpBackwards) {
newIndex = currentIndex;
} else {
newIndex = currentIndex + 1;
}
}
}
this._updateSearchCount(this._currentSearchTreeElements.length);
if (!this._currentSearchTreeElements.length) {
this._updateSearchIndex(0);
return;
}
newIndex = mod(newIndex, this._currentSearchTreeElements.length);
this._jumpToMatch(newIndex, shouldJump);
}
_innerSearchCanceled() {
for (let element = this._treeOutline.rootElement(); element; element = element.traverseNextTreeElement(false)) {
if (!(element instanceof XMLViewNode)) {
continue;
}
element.revertHighlightChanges();
}
this._updateSearchCount(0);
this._updateSearchIndex(0);
}
/**
* @override
*/
searchCanceled() {
this._searchConfig = null;
this._currentSearchTreeElements = [];
this._innerSearchCanceled();
}
/**
* @override
* @param {!UI.SearchableView.SearchConfig} searchConfig
* @param {boolean} shouldJump
* @param {boolean=} jumpBackwards
*/
performSearch(searchConfig, shouldJump, jumpBackwards) {
this._searchConfig = searchConfig;
this._innerPerformSearch(shouldJump, jumpBackwards);
}
/**
* @override
*/
jumpToNextSearchResult() {
if (!this._currentSearchTreeElements.length) {
return;
}
const newIndex = mod(this._currentSearchFocusIndex + 1, this._currentSearchTreeElements.length);
this._jumpToMatch(newIndex, true);
}
/**
* @override
*/
jumpToPreviousSearchResult() {
if (!this._currentSearchTreeElements.length) {
return;
}
const newIndex = mod(this._currentSearchFocusIndex - 1, this._currentSearchTreeElements.length);
this._jumpToMatch(newIndex, true);
}
/**
* @override
* @return {boolean}
*/
supportsCaseSensitiveSearch() {
return true;
}
/**
* @override
* @return {boolean}
*/
supportsRegexSearch() {
return true;
}
}
/**
* @unrestricted
*/
export class XMLViewNode extends UI.TreeElement {
/**
* @param {!Node} node
* @param {boolean} closeTag
* @param {!XMLView} xmlView
*/
constructor(node, closeTag, xmlView) {
super('', !closeTag && !!node.childElementCount);
this._node = node;
this._closeTag = closeTag;
this.selectable = true;
/** @type {!Array.<!Object>} */
this._highlightChanges = [];
this._xmlView = xmlView;
this._updateTitle();
}
/**
* @param {!UI.TreeOutline|!UI.TreeElement} root
* @param {!Node} xmlNode
* @param {!XMLView} xmlView
*/
static populate(root, xmlNode, xmlView) {
let node = xmlNode.firstChild;
while (node) {
const currentNode = node;
node = node.nextSibling;
const nodeType = currentNode.nodeType;
// ignore empty TEXT
if (nodeType === 3 && currentNode.nodeValue.match(/\s+/)) {
continue;
}
// ignore ATTRIBUTE, ENTITY_REFERENCE, ENTITY, DOCUMENT, DOCUMENT_TYPE, DOCUMENT_FRAGMENT, NOTATION
if ((nodeType !== 1) && (nodeType !== 3) && (nodeType !== 4) && (nodeType !== 7) && (nodeType !== 8)) {
continue;
}
root.appendChild(new XMLViewNode(currentNode, false, xmlView));
}
}
/**
* @param {?RegExp} regex
* @param {string=} additionalCssClassName
* @return {boolean}
*/
setSearchRegex(regex, additionalCssClassName) {
this.revertHighlightChanges();
if (!regex) {
return false;
}
if (this._closeTag && this.parent && !this.parent.expanded) {
return false;
}
regex.lastIndex = 0;
let cssClasses = UI.highlightedSearchResultClassName;
if (additionalCssClassName) {
cssClasses += ' ' + additionalCssClassName;
}
const content = this.listItemElement.textContent.replace(/\xA0/g, ' ');
let match = regex.exec(content);
const ranges = [];
while (match) {
ranges.push(new TextUtils.SourceRange(match.index, match[0].length));
match = regex.exec(content);
}
if (ranges.length) {
UI.highlightRangesWithStyleClass(this.listItemElement, ranges, cssClasses, this._highlightChanges);
}
return !!this._highlightChanges.length;
}
revertHighlightChanges() {
UI.revertDomChanges(this._highlightChanges);
this._highlightChanges = [];
}
_updateTitle() {
const node = this._node;
switch (node.nodeType) {
case 1: // ELEMENT
const tag = node.tagName;
if (this._closeTag) {
this._setTitle(['</' + tag + '>', 'shadow-xml-view-tag']);
return;
}
const titleItems = ['<' + tag, 'shadow-xml-view-tag'];
const attributes = node.attributes;
for (let i = 0; i < attributes.length; ++i) {
const attributeNode = attributes.item(i);
titleItems.push(
'\xA0', 'shadow-xml-view-tag', attributeNode.name, 'shadow-xml-view-attribute-name', '="',
'shadow-xml-view-tag', attributeNode.value, 'shadow-xml-view-attribute-value', '"',
'shadow-xml-view-tag');
}
if (!this.expanded) {
if (node.childElementCount) {
titleItems.push(
'>', 'shadow-xml-view-tag', '\u2026', 'shadow-xml-view-comment', '</' + tag, 'shadow-xml-view-tag');
} else if (this._node.textContent) {
titleItems.push(
'>', 'shadow-xml-view-tag', node.textContent, 'shadow-xml-view-text', '</' + tag,
'shadow-xml-view-tag');
} else {
titleItems.push(' /', 'shadow-xml-view-tag');
}
}
titleItems.push('>', 'shadow-xml-view-tag');
this._setTitle(titleItems);
return;
case 3: // TEXT
this._setTitle([node.nodeValue, 'shadow-xml-view-text']);
return;
case 4: // CDATA
this._setTitle([
'<![CDATA[', 'shadow-xml-view-cdata', node.nodeValue, 'shadow-xml-view-text', ']]>', 'shadow-xml-view-cdata'
]);
return;
case 7: // PROCESSING_INSTRUCTION
this._setTitle(['<?' + node.nodeName + ' ' + node.nodeValue + '?>', 'shadow-xml-view-processing-instruction']);
return;
case 8: // COMMENT
this._setTitle(['<!--' + node.nodeValue + '-->', 'shadow-xml-view-comment']);
return;
}
}
/**
* @param {!Array.<string>} items
*/
_setTitle(items) {
const titleFragment = createDocumentFragment();
for (let i = 0; i < items.length; i += 2) {
titleFragment.createChild('span', items[i + 1]).textContent = items[i];
}
this.title = titleFragment;
this._xmlView._innerPerformSearch(false, false);
}
/**
* @override
*/
onattach() {
this.listItemElement.classList.toggle('shadow-xml-view-close-tag', this._closeTag);
}
/**
* @override
*/
onexpand() {
this._updateTitle();
}
/**
* @override
*/
oncollapse() {
this._updateTitle();
}
/**
* @override
* @returns {!Promise}
*/
async onpopulate() {
XMLViewNode.populate(this, this._node, this._xmlView);
this.appendChild(new XMLViewNode(this._node, true, this._xmlView));
}
}
/* Legacy exported object */
self.SourceFrame = self.SourceFrame || {};
/* Legacy exported object */
SourceFrame = SourceFrame || {};
/** @constructor */
SourceFrame.XMLView = XMLView;
/** @constructor */
SourceFrame.XMLView.Node = XMLViewNode;