175 lines
		
	
	
	
		
			7.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			175 lines
		
	
	
	
		
			7.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|   | /*--------------------------------------------------------------------------------------------- | ||
|  |  *  Copyright (c) Microsoft Corporation. All rights reserved. | ||
|  |  *  Licensed under the MIT License. See License.txt in the project root for license information. | ||
|  |  *--------------------------------------------------------------------------------------------*/ | ||
|  | import { createScanner } from '../parser/htmlScanner'; | ||
|  | import { TokenType, Range, SelectionRange } from '../htmlLanguageTypes'; | ||
|  | export class HTMLSelectionRange { | ||
|  |     constructor(htmlParser) { | ||
|  |         this.htmlParser = htmlParser; | ||
|  |     } | ||
|  |     getSelectionRanges(document, positions) { | ||
|  |         const htmlDocument = this.htmlParser.parseDocument(document); | ||
|  |         return positions.map(p => this.getSelectionRange(p, document, htmlDocument)); | ||
|  |     } | ||
|  |     getSelectionRange(position, document, htmlDocument) { | ||
|  |         const applicableRanges = this.getApplicableRanges(document, position, htmlDocument); | ||
|  |         let prev = undefined; | ||
|  |         let current = undefined; | ||
|  |         for (let index = applicableRanges.length - 1; index >= 0; index--) { | ||
|  |             const range = applicableRanges[index]; | ||
|  |             if (!prev || range[0] !== prev[0] || range[1] !== prev[1]) { | ||
|  |                 current = SelectionRange.create(Range.create(document.positionAt(applicableRanges[index][0]), document.positionAt(applicableRanges[index][1])), current); | ||
|  |             } | ||
|  |             prev = range; | ||
|  |         } | ||
|  |         if (!current) { | ||
|  |             current = SelectionRange.create(Range.create(position, position)); | ||
|  |         } | ||
|  |         return current; | ||
|  |     } | ||
|  |     getApplicableRanges(document, position, htmlDoc) { | ||
|  |         const currOffset = document.offsetAt(position); | ||
|  |         const currNode = htmlDoc.findNodeAt(currOffset); | ||
|  |         let result = this.getAllParentTagRanges(currNode); | ||
|  |         // Self-closing or void elements
 | ||
|  |         if (currNode.startTagEnd && !currNode.endTagStart) { | ||
|  |             // THe rare case of unmatching tag pairs like <div></div1>
 | ||
|  |             if (currNode.startTagEnd !== currNode.end) { | ||
|  |                 return [[currNode.start, currNode.end]]; | ||
|  |             } | ||
|  |             const closeRange = Range.create(document.positionAt(currNode.startTagEnd - 2), document.positionAt(currNode.startTagEnd)); | ||
|  |             const closeText = document.getText(closeRange); | ||
|  |             // Self-closing element
 | ||
|  |             if (closeText === '/>') { | ||
|  |                 result.unshift([currNode.start + 1, currNode.startTagEnd - 2]); | ||
|  |             } | ||
|  |             // Void element
 | ||
|  |             else { | ||
|  |                 result.unshift([currNode.start + 1, currNode.startTagEnd - 1]); | ||
|  |             } | ||
|  |             const attributeLevelRanges = this.getAttributeLevelRanges(document, currNode, currOffset); | ||
|  |             result = attributeLevelRanges.concat(result); | ||
|  |             return result; | ||
|  |         } | ||
|  |         if (!currNode.startTagEnd || !currNode.endTagStart) { | ||
|  |             return result; | ||
|  |         } | ||
|  |         /** | ||
|  |          * For html like | ||
|  |          * `<div class="foo">bar</div>` | ||
|  |          */ | ||
|  |         result.unshift([currNode.start, currNode.end]); | ||
|  |         /** | ||
|  |          * Cursor inside `<div class="foo">` | ||
|  |          */ | ||
|  |         if (currNode.start < currOffset && currOffset < currNode.startTagEnd) { | ||
|  |             result.unshift([currNode.start + 1, currNode.startTagEnd - 1]); | ||
|  |             const attributeLevelRanges = this.getAttributeLevelRanges(document, currNode, currOffset); | ||
|  |             result = attributeLevelRanges.concat(result); | ||
|  |             return result; | ||
|  |         } | ||
|  |         /** | ||
|  |          * Cursor inside `bar` | ||
|  |          */ | ||
|  |         else if (currNode.startTagEnd <= currOffset && currOffset <= currNode.endTagStart) { | ||
|  |             result.unshift([currNode.startTagEnd, currNode.endTagStart]); | ||
|  |             return result; | ||
|  |         } | ||
|  |         /** | ||
|  |          * Cursor inside `</div>` | ||
|  |          */ | ||
|  |         else { | ||
|  |             // `div` inside `</div>`
 | ||
|  |             if (currOffset >= currNode.endTagStart + 2) { | ||
|  |                 result.unshift([currNode.endTagStart + 2, currNode.end - 1]); | ||
|  |             } | ||
|  |             return result; | ||
|  |         } | ||
|  |     } | ||
|  |     getAllParentTagRanges(initialNode) { | ||
|  |         let currNode = initialNode; | ||
|  |         const result = []; | ||
|  |         while (currNode.parent) { | ||
|  |             currNode = currNode.parent; | ||
|  |             this.getNodeRanges(currNode).forEach(r => result.push(r)); | ||
|  |         } | ||
|  |         return result; | ||
|  |     } | ||
|  |     getNodeRanges(n) { | ||
|  |         if (n.startTagEnd && n.endTagStart && n.startTagEnd < n.endTagStart) { | ||
|  |             return [ | ||
|  |                 [n.startTagEnd, n.endTagStart], | ||
|  |                 [n.start, n.end] | ||
|  |             ]; | ||
|  |         } | ||
|  |         return [ | ||
|  |             [n.start, n.end] | ||
|  |         ]; | ||
|  |     } | ||
|  |     ; | ||
|  |     getAttributeLevelRanges(document, currNode, currOffset) { | ||
|  |         const currNodeRange = Range.create(document.positionAt(currNode.start), document.positionAt(currNode.end)); | ||
|  |         const currNodeText = document.getText(currNodeRange); | ||
|  |         const relativeOffset = currOffset - currNode.start; | ||
|  |         /** | ||
|  |          * Tag level semantic selection | ||
|  |          */ | ||
|  |         const scanner = createScanner(currNodeText); | ||
|  |         let token = scanner.scan(); | ||
|  |         /** | ||
|  |          * For text like | ||
|  |          * <div class="foo">bar</div> | ||
|  |          */ | ||
|  |         const positionOffset = currNode.start; | ||
|  |         const result = []; | ||
|  |         let isInsideAttribute = false; | ||
|  |         let attrStart = -1; | ||
|  |         while (token !== TokenType.EOS) { | ||
|  |             switch (token) { | ||
|  |                 case TokenType.AttributeName: { | ||
|  |                     if (relativeOffset < scanner.getTokenOffset()) { | ||
|  |                         isInsideAttribute = false; | ||
|  |                         break; | ||
|  |                     } | ||
|  |                     if (relativeOffset <= scanner.getTokenEnd()) { | ||
|  |                         // `class`
 | ||
|  |                         result.unshift([scanner.getTokenOffset(), scanner.getTokenEnd()]); | ||
|  |                     } | ||
|  |                     isInsideAttribute = true; | ||
|  |                     attrStart = scanner.getTokenOffset(); | ||
|  |                     break; | ||
|  |                 } | ||
|  |                 case TokenType.AttributeValue: { | ||
|  |                     if (!isInsideAttribute) { | ||
|  |                         break; | ||
|  |                     } | ||
|  |                     const valueText = scanner.getTokenText(); | ||
|  |                     if (relativeOffset < scanner.getTokenOffset()) { | ||
|  |                         // `class="foo"`
 | ||
|  |                         result.push([attrStart, scanner.getTokenEnd()]); | ||
|  |                         break; | ||
|  |                     } | ||
|  |                     if (relativeOffset >= scanner.getTokenOffset() && relativeOffset <= scanner.getTokenEnd()) { | ||
|  |                         // `"foo"`
 | ||
|  |                         result.unshift([scanner.getTokenOffset(), scanner.getTokenEnd()]); | ||
|  |                         // `foo`
 | ||
|  |                         if ((valueText[0] === `"` && valueText[valueText.length - 1] === `"`) || (valueText[0] === `'` && valueText[valueText.length - 1] === `'`)) { | ||
|  |                             if (relativeOffset >= scanner.getTokenOffset() + 1 && relativeOffset <= scanner.getTokenEnd() - 1) { | ||
|  |                                 result.unshift([scanner.getTokenOffset() + 1, scanner.getTokenEnd() - 1]); | ||
|  |                             } | ||
|  |                         } | ||
|  |                         // `class="foo"`
 | ||
|  |                         result.push([attrStart, scanner.getTokenEnd()]); | ||
|  |                     } | ||
|  |                     break; | ||
|  |                 } | ||
|  |             } | ||
|  |             token = scanner.scan(); | ||
|  |         } | ||
|  |         return result.map(pair => { | ||
|  |             return [pair[0] + positionOffset, pair[1] + positionOffset]; | ||
|  |         }); | ||
|  |     } | ||
|  | } |