| // 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. |
| |
| /* eslint-disable rulesdir/no_underscored_properties */ |
| |
| import * as i18n from '../../../../core/i18n/i18n.js'; |
| import * as Platform from '../../../../core/platform/platform.js'; |
| import * as TextUtils from '../../../../models/text_utils/text_utils.js'; |
| import * as UI from '../../legacy.js'; |
| |
| const UIStrings = { |
| /** |
| *@description Text to find an item |
| */ |
| find: 'Find', |
| }; |
| const str_ = i18n.i18n.registerUIStrings('ui/legacy/components/source_frame/XMLView.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| export class XMLView extends UI.Widget.Widget implements UI.SearchableView.Searchable { |
| _treeOutline: UI.TreeOutline.TreeOutlineInShadow; |
| _searchableView!: UI.SearchableView.SearchableView|null; |
| _currentSearchFocusIndex: number; |
| _currentSearchTreeElements: XMLViewNode[]; |
| _searchConfig!: UI.SearchableView.SearchConfig|null; |
| |
| constructor(parsedXML: Document) { |
| super(true); |
| this.registerRequiredCSS('ui/legacy/components/source_frame/xmlView.css', {enableLegacyPatching: false}); |
| this.contentElement.classList.add('shadow-xml-view', 'source-code'); |
| this._treeOutline = new UI.TreeOutline.TreeOutlineInShadow(); |
| this._treeOutline.registerRequiredCSS( |
| 'ui/legacy/components/source_frame/xmlTree.css', {enableLegacyPatching: true}); |
| this.contentElement.appendChild(this._treeOutline.element); |
| this._currentSearchFocusIndex = 0; |
| this._currentSearchTreeElements = []; |
| |
| XMLViewNode.populate(this._treeOutline, parsedXML, this); |
| const firstChild = this._treeOutline.firstChild(); |
| if (firstChild) { |
| firstChild.select(true /* omitFocus */, false /* selectedByUser */); |
| } |
| } |
| |
| static createSearchableView(parsedXML: Document): UI.SearchableView.SearchableView { |
| const xmlView = new XMLView(parsedXML); |
| const searchableView = new UI.SearchableView.SearchableView(xmlView, null); |
| searchableView.setPlaceholder(i18nString(UIStrings.find)); |
| xmlView._searchableView = searchableView; |
| xmlView.show(searchableView.element); |
| return searchableView; |
| } |
| |
| static parseXML(text: string, mimeType: string): Document|null { |
| let parsedXML; |
| try { |
| switch (mimeType) { |
| case 'application/xhtml+xml': |
| case 'application/xml': |
| case 'image/svg+xml': |
| case 'text/html': |
| case 'text/xml': |
| parsedXML = (new DOMParser()).parseFromString(text, mimeType); |
| } |
| } catch (e) { |
| return null; |
| } |
| if (!parsedXML || parsedXML.body) { |
| return null; |
| } |
| return parsedXML; |
| } |
| |
| _jumpToMatch(index: number, shouldJump: boolean): void { |
| 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.UIUtils.highlightedCurrentSearchResultClassName); |
| } else { |
| this._updateSearchIndex(0); |
| } |
| } |
| |
| _updateSearchCount(count: number): void { |
| if (!this._searchableView) { |
| return; |
| } |
| this._searchableView.updateSearchMatchesCount(count); |
| } |
| |
| _updateSearchIndex(index: number): void { |
| this._currentSearchFocusIndex = index; |
| if (!this._searchableView) { |
| return; |
| } |
| this._searchableView.updateCurrentMatchIndex(index); |
| } |
| |
| _innerPerformSearch(shouldJump: boolean, jumpBackwards?: boolean): void { |
| if (!this._searchConfig) { |
| return; |
| } |
| let newIndex: number = this._currentSearchFocusIndex; |
| const previousSearchFocusElement = this._currentSearchTreeElements[newIndex]; |
| this._innerSearchCanceled(); |
| this._currentSearchTreeElements = []; |
| const regex = this._searchConfig.toSearchRegex(true); |
| |
| for (let element: (UI.TreeOutline.TreeElement|null) = |
| (this._treeOutline.rootElement() as UI.TreeOutline.TreeElement | null); |
| 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 = Platform.NumberUtilities.mod(newIndex, this._currentSearchTreeElements.length); |
| |
| this._jumpToMatch(newIndex, shouldJump); |
| } |
| |
| _innerSearchCanceled(): void { |
| for (let element: (UI.TreeOutline.TreeElement|null) = |
| (this._treeOutline.rootElement() as UI.TreeOutline.TreeElement | null); |
| element; element = element.traverseNextTreeElement(false)) { |
| if (!(element instanceof XMLViewNode)) { |
| continue; |
| } |
| element.revertHighlightChanges(); |
| } |
| this._updateSearchCount(0); |
| this._updateSearchIndex(0); |
| } |
| |
| searchCanceled(): void { |
| this._searchConfig = null; |
| this._currentSearchTreeElements = []; |
| this._innerSearchCanceled(); |
| } |
| |
| performSearch(searchConfig: UI.SearchableView.SearchConfig, shouldJump: boolean, jumpBackwards?: boolean): void { |
| this._searchConfig = searchConfig; |
| this._innerPerformSearch(shouldJump, jumpBackwards); |
| } |
| |
| jumpToNextSearchResult(): void { |
| if (!this._currentSearchTreeElements.length) { |
| return; |
| } |
| |
| const newIndex = |
| Platform.NumberUtilities.mod(this._currentSearchFocusIndex + 1, this._currentSearchTreeElements.length); |
| this._jumpToMatch(newIndex, true); |
| } |
| |
| jumpToPreviousSearchResult(): void { |
| if (!this._currentSearchTreeElements.length) { |
| return; |
| } |
| |
| const newIndex = |
| Platform.NumberUtilities.mod(this._currentSearchFocusIndex - 1, this._currentSearchTreeElements.length); |
| this._jumpToMatch(newIndex, true); |
| } |
| |
| supportsCaseSensitiveSearch(): boolean { |
| return true; |
| } |
| |
| supportsRegexSearch(): boolean { |
| return true; |
| } |
| } |
| |
| export class XMLViewNode extends UI.TreeOutline.TreeElement { |
| _node: Node|ParentNode; |
| _closeTag: boolean; |
| _highlightChanges: UI.UIUtils.HighlightChange[]; |
| _xmlView: XMLView; |
| |
| constructor(node: Node|ParentNode, closeTag: boolean, xmlView: XMLView) { |
| super('', !closeTag && 'childElementCount' in node && Boolean(node.childElementCount)); |
| this._node = node; |
| this._closeTag = closeTag; |
| this.selectable = true; |
| this._highlightChanges = []; |
| this._xmlView = xmlView; |
| this._updateTitle(); |
| } |
| |
| static populate( |
| root: UI.TreeOutline.TreeOutline|UI.TreeOutline.TreeElement, xmlNode: Node|ParentNode, xmlView: XMLView): void { |
| if (!(xmlNode instanceof Node)) { |
| return; |
| } |
| let node: (ChildNode|null) = xmlNode.firstChild; |
| while (node) { |
| const currentNode = node; |
| node = node.nextSibling; |
| const nodeType = currentNode.nodeType; |
| // ignore empty TEXT |
| if (nodeType === 3 && currentNode.nodeValue && 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)); |
| } |
| } |
| |
| setSearchRegex(regex: RegExp|null, additionalCssClassName?: string): boolean { |
| this.revertHighlightChanges(); |
| if (!regex) { |
| return false; |
| } |
| if (this._closeTag && this.parent && !this.parent.expanded) { |
| return false; |
| } |
| regex.lastIndex = 0; |
| let cssClasses = UI.UIUtils.highlightedSearchResultClassName; |
| if (additionalCssClassName) { |
| cssClasses += ' ' + additionalCssClassName; |
| } |
| if (!this.listItemElement.textContent) { |
| return false; |
| } |
| const content = this.listItemElement.textContent.replace(/\xA0/g, ' '); |
| let match = regex.exec(content); |
| const ranges = []; |
| while (match) { |
| ranges.push(new TextUtils.TextRange.SourceRange(match.index, match[0].length)); |
| match = regex.exec(content); |
| } |
| if (ranges.length) { |
| UI.UIUtils.highlightRangesWithStyleClass(this.listItemElement, ranges, cssClasses, this._highlightChanges); |
| } |
| return Boolean(this._highlightChanges.length); |
| } |
| |
| revertHighlightChanges(): void { |
| UI.UIUtils.revertDomChanges(this._highlightChanges); |
| this._highlightChanges = []; |
| } |
| |
| _updateTitle(): void { |
| const node = this._node; |
| if (!('nodeType' in node)) { |
| return; |
| } |
| switch (node.nodeType) { |
| case 1: { // ELEMENT |
| if (node instanceof 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); |
| if (!attributeNode) { |
| return; |
| } |
| 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', '…', 'shadow-xml-view-comment', '</' + tag, 'shadow-xml-view-tag'); |
| } else if (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; |
| } |
| return; |
| } |
| case 3: { // TEXT |
| if (node.nodeValue) { |
| this._setTitle([node.nodeValue, 'shadow-xml-view-text']); |
| } |
| return; |
| } |
| case 4: { // CDATA |
| if (node.nodeValue) { |
| this._setTitle([ |
| '<![CDATA[', |
| 'shadow-xml-view-cdata', |
| node.nodeValue, |
| 'shadow-xml-view-text', |
| ']]>', |
| 'shadow-xml-view-cdata', |
| ]); |
| } |
| return; |
| } |
| case 7: { // PROCESSING_INSTRUCTION |
| if (node.nodeValue) { |
| this._setTitle( |
| ['<?' + node.nodeName + ' ' + node.nodeValue + '?>', 'shadow-xml-view-processing-instruction']); |
| } |
| return; |
| } |
| case 8: { // COMMENT |
| this._setTitle(['<!--' + node.nodeValue + '-->', 'shadow-xml-view-comment']); |
| return; |
| } |
| } |
| } |
| |
| _setTitle(items: string[]): void { |
| const titleFragment = document.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); |
| } |
| |
| onattach(): void { |
| this.listItemElement.classList.toggle('shadow-xml-view-close-tag', this._closeTag); |
| } |
| |
| onexpand(): void { |
| this._updateTitle(); |
| } |
| |
| oncollapse(): void { |
| this._updateTitle(); |
| } |
| |
| async onpopulate(): Promise<void> { |
| XMLViewNode.populate(this, this._node, this._xmlView); |
| this.appendChild(new XMLViewNode(this._node, true, this._xmlView)); |
| } |
| } |