UNPKG

chrome-devtools-frontend

Version:
376 lines (339 loc) 12.3 kB
// 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-imperative-dom-api */ 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'; import xmlTreeStyles from './xmlTree.css.js'; import xmlViewStyles from './xmlView.css.js'; const UIStrings = { /** *@description Text to find an item */ find: 'Find', } as const; 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 { private readonly treeOutline = new UI.TreeOutline.TreeOutlineInShadow(); private searchableView: UI.SearchableView.SearchableView|null = null; private currentSearchFocusIndex = 0; private currentSearchTreeElements: XMLViewNode[] = []; private searchConfig: UI.SearchableView.SearchConfig|null = null; constructor(parsedXML: Document) { super(true); this.registerRequiredCSS(xmlViewStyles); this.contentElement.classList.add('shadow-xml-view', 'source-code'); this.treeOutline.registerRequiredCSS(xmlTreeStyles); this.contentElement.appendChild(this.treeOutline.element); 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 { return null; } if (!parsedXML || parsedXML.body) { return null; } return parsedXML; } private 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); } } private updateSearchCount(count: number): void { if (!this.searchableView) { return; } this.searchableView.updateSearchMatchesCount(count); } private 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); } private 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); } onSearchCanceled(): 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 { private readonly node: Node|ParentNode; private readonly closeTag: boolean; private highlightChanges: UI.UIUtils.HighlightChange[]; private readonly 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?.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 = []; } private 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; } } } private 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); } override onattach(): void { this.listItemElement.classList.toggle('shadow-xml-view-close-tag', this.closeTag); } override onexpand(): void { this.updateTitle(); } override oncollapse(): void { this.updateTitle(); } override async onpopulate(): Promise<void> { XMLViewNode.populate(this, this.node, this.xmlView); this.appendChild(new XMLViewNode(this.node, true, this.xmlView)); } }