266 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			266 lines
		
	
	
	
		
			11 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, Position, MarkupKind } from '../htmlLanguageTypes'; | ||
|  | import { isDefined } from '../utils/object'; | ||
|  | import { generateDocumentation } from '../languageFacts/dataProvider'; | ||
|  | import { entities } from '../parser/htmlEntities'; | ||
|  | import { isLetterOrDigit } from '../utils/strings'; | ||
|  | import * as l10n from '@vscode/l10n'; | ||
|  | export class HTMLHover { | ||
|  |     constructor(lsOptions, dataManager) { | ||
|  |         this.lsOptions = lsOptions; | ||
|  |         this.dataManager = dataManager; | ||
|  |     } | ||
|  |     doHover(document, position, htmlDocument, options) { | ||
|  |         const convertContents = this.convertContents.bind(this); | ||
|  |         const doesSupportMarkdown = this.doesSupportMarkdown(); | ||
|  |         const offset = document.offsetAt(position); | ||
|  |         const node = htmlDocument.findNodeAt(offset); | ||
|  |         const text = document.getText(); | ||
|  |         if (!node || !node.tag) { | ||
|  |             return null; | ||
|  |         } | ||
|  |         const dataProviders = this.dataManager.getDataProviders().filter(p => p.isApplicable(document.languageId)); | ||
|  |         function getTagHover(currTag, range, open) { | ||
|  |             for (const provider of dataProviders) { | ||
|  |                 let hover = null; | ||
|  |                 provider.provideTags().forEach(tag => { | ||
|  |                     if (tag.name.toLowerCase() === currTag.toLowerCase()) { | ||
|  |                         let markupContent = generateDocumentation(tag, options, doesSupportMarkdown); | ||
|  |                         if (!markupContent) { | ||
|  |                             markupContent = { | ||
|  |                                 kind: doesSupportMarkdown ? 'markdown' : 'plaintext', | ||
|  |                                 value: '' | ||
|  |                             }; | ||
|  |                         } | ||
|  |                         hover = { contents: markupContent, range }; | ||
|  |                     } | ||
|  |                 }); | ||
|  |                 if (hover) { | ||
|  |                     hover.contents = convertContents(hover.contents); | ||
|  |                     return hover; | ||
|  |                 } | ||
|  |             } | ||
|  |             return null; | ||
|  |         } | ||
|  |         function getAttrHover(currTag, currAttr, range) { | ||
|  |             for (const provider of dataProviders) { | ||
|  |                 let hover = null; | ||
|  |                 provider.provideAttributes(currTag).forEach(attr => { | ||
|  |                     if (currAttr === attr.name && attr.description) { | ||
|  |                         const contentsDoc = generateDocumentation(attr, options, doesSupportMarkdown); | ||
|  |                         if (contentsDoc) { | ||
|  |                             hover = { contents: contentsDoc, range }; | ||
|  |                         } | ||
|  |                         else { | ||
|  |                             hover = null; | ||
|  |                         } | ||
|  |                     } | ||
|  |                 }); | ||
|  |                 if (hover) { | ||
|  |                     hover.contents = convertContents(hover.contents); | ||
|  |                     return hover; | ||
|  |                 } | ||
|  |             } | ||
|  |             return null; | ||
|  |         } | ||
|  |         function getAttrValueHover(currTag, currAttr, currAttrValue, range) { | ||
|  |             for (const provider of dataProviders) { | ||
|  |                 let hover = null; | ||
|  |                 provider.provideValues(currTag, currAttr).forEach(attrValue => { | ||
|  |                     if (currAttrValue === attrValue.name && attrValue.description) { | ||
|  |                         const contentsDoc = generateDocumentation(attrValue, options, doesSupportMarkdown); | ||
|  |                         if (contentsDoc) { | ||
|  |                             hover = { contents: contentsDoc, range }; | ||
|  |                         } | ||
|  |                         else { | ||
|  |                             hover = null; | ||
|  |                         } | ||
|  |                     } | ||
|  |                 }); | ||
|  |                 if (hover) { | ||
|  |                     hover.contents = convertContents(hover.contents); | ||
|  |                     return hover; | ||
|  |                 } | ||
|  |             } | ||
|  |             return null; | ||
|  |         } | ||
|  |         function getEntityHover(text, range) { | ||
|  |             let currEntity = filterEntity(text); | ||
|  |             for (const entity in entities) { | ||
|  |                 let hover = null; | ||
|  |                 const label = '&' + entity; | ||
|  |                 if (currEntity === label) { | ||
|  |                     let code = entities[entity].charCodeAt(0).toString(16).toUpperCase(); | ||
|  |                     let hex = 'U+'; | ||
|  |                     if (code.length < 4) { | ||
|  |                         const zeroes = 4 - code.length; | ||
|  |                         let k = 0; | ||
|  |                         while (k < zeroes) { | ||
|  |                             hex += '0'; | ||
|  |                             k += 1; | ||
|  |                         } | ||
|  |                     } | ||
|  |                     hex += code; | ||
|  |                     const contentsDoc = l10n.t('Character entity representing \'{0}\', unicode equivalent \'{1}\'', entities[entity], hex); | ||
|  |                     if (contentsDoc) { | ||
|  |                         hover = { contents: contentsDoc, range }; | ||
|  |                     } | ||
|  |                     else { | ||
|  |                         hover = null; | ||
|  |                     } | ||
|  |                 } | ||
|  |                 if (hover) { | ||
|  |                     hover.contents = convertContents(hover.contents); | ||
|  |                     return hover; | ||
|  |                 } | ||
|  |             } | ||
|  |             return null; | ||
|  |         } | ||
|  |         function getTagNameRange(tokenType, startOffset) { | ||
|  |             const scanner = createScanner(document.getText(), startOffset); | ||
|  |             let token = scanner.scan(); | ||
|  |             while (token !== TokenType.EOS && (scanner.getTokenEnd() < offset || scanner.getTokenEnd() === offset && token !== tokenType)) { | ||
|  |                 token = scanner.scan(); | ||
|  |             } | ||
|  |             if (token === tokenType && offset <= scanner.getTokenEnd()) { | ||
|  |                 return { start: document.positionAt(scanner.getTokenOffset()), end: document.positionAt(scanner.getTokenEnd()) }; | ||
|  |             } | ||
|  |             return null; | ||
|  |         } | ||
|  |         function getEntityRange() { | ||
|  |             let k = offset - 1; | ||
|  |             let characterStart = position.character; | ||
|  |             while (k >= 0 && isLetterOrDigit(text, k)) { | ||
|  |                 k--; | ||
|  |                 characterStart--; | ||
|  |             } | ||
|  |             let n = k + 1; | ||
|  |             let characterEnd = characterStart; | ||
|  |             while (isLetterOrDigit(text, n)) { | ||
|  |                 n++; | ||
|  |                 characterEnd++; | ||
|  |             } | ||
|  |             if (k >= 0 && text[k] === '&') { | ||
|  |                 let range = null; | ||
|  |                 if (text[n] === ';') { | ||
|  |                     range = Range.create(Position.create(position.line, characterStart), Position.create(position.line, characterEnd + 1)); | ||
|  |                 } | ||
|  |                 else { | ||
|  |                     range = Range.create(Position.create(position.line, characterStart), Position.create(position.line, characterEnd)); | ||
|  |                 } | ||
|  |                 return range; | ||
|  |             } | ||
|  |             return null; | ||
|  |         } | ||
|  |         function filterEntity(text) { | ||
|  |             let k = offset - 1; | ||
|  |             let newText = '&'; | ||
|  |             while (k >= 0 && isLetterOrDigit(text, k)) { | ||
|  |                 k--; | ||
|  |             } | ||
|  |             k = k + 1; | ||
|  |             while (isLetterOrDigit(text, k)) { | ||
|  |                 newText += text[k]; | ||
|  |                 k += 1; | ||
|  |             } | ||
|  |             newText += ';'; | ||
|  |             return newText; | ||
|  |         } | ||
|  |         if (node.endTagStart && offset >= node.endTagStart) { | ||
|  |             const tagRange = getTagNameRange(TokenType.EndTag, node.endTagStart); | ||
|  |             if (tagRange) { | ||
|  |                 return getTagHover(node.tag, tagRange, false); | ||
|  |             } | ||
|  |             return null; | ||
|  |         } | ||
|  |         const tagRange = getTagNameRange(TokenType.StartTag, node.start); | ||
|  |         if (tagRange) { | ||
|  |             return getTagHover(node.tag, tagRange, true); | ||
|  |         } | ||
|  |         const attrRange = getTagNameRange(TokenType.AttributeName, node.start); | ||
|  |         if (attrRange) { | ||
|  |             const tag = node.tag; | ||
|  |             const attr = document.getText(attrRange); | ||
|  |             return getAttrHover(tag, attr, attrRange); | ||
|  |         } | ||
|  |         const entityRange = getEntityRange(); | ||
|  |         if (entityRange) { | ||
|  |             return getEntityHover(text, entityRange); | ||
|  |         } | ||
|  |         function scanAttrAndAttrValue(nodeStart, attrValueStart) { | ||
|  |             const scanner = createScanner(document.getText(), nodeStart); | ||
|  |             let token = scanner.scan(); | ||
|  |             let prevAttr = undefined; | ||
|  |             while (token !== TokenType.EOS && (scanner.getTokenEnd() <= attrValueStart)) { | ||
|  |                 token = scanner.scan(); | ||
|  |                 if (token === TokenType.AttributeName) { | ||
|  |                     prevAttr = scanner.getTokenText(); | ||
|  |                 } | ||
|  |             } | ||
|  |             return prevAttr; | ||
|  |         } | ||
|  |         const attrValueRange = getTagNameRange(TokenType.AttributeValue, node.start); | ||
|  |         if (attrValueRange) { | ||
|  |             const tag = node.tag; | ||
|  |             const attrValue = trimQuotes(document.getText(attrValueRange)); | ||
|  |             const matchAttr = scanAttrAndAttrValue(node.start, document.offsetAt(attrValueRange.start)); | ||
|  |             if (matchAttr) { | ||
|  |                 return getAttrValueHover(tag, matchAttr, attrValue, attrValueRange); | ||
|  |             } | ||
|  |         } | ||
|  |         return null; | ||
|  |     } | ||
|  |     convertContents(contents) { | ||
|  |         if (!this.doesSupportMarkdown()) { | ||
|  |             if (typeof contents === 'string') { | ||
|  |                 return contents; | ||
|  |             } | ||
|  |             // MarkupContent
 | ||
|  |             else if ('kind' in contents) { | ||
|  |                 return { | ||
|  |                     kind: 'plaintext', | ||
|  |                     value: contents.value | ||
|  |                 }; | ||
|  |             } | ||
|  |             // MarkedString[]
 | ||
|  |             else if (Array.isArray(contents)) { | ||
|  |                 contents.map(c => { | ||
|  |                     return typeof c === 'string' ? c : c.value; | ||
|  |                 }); | ||
|  |             } | ||
|  |             // MarkedString
 | ||
|  |             else { | ||
|  |                 return contents.value; | ||
|  |             } | ||
|  |         } | ||
|  |         return contents; | ||
|  |     } | ||
|  |     doesSupportMarkdown() { | ||
|  |         if (!isDefined(this.supportsMarkdown)) { | ||
|  |             if (!isDefined(this.lsOptions.clientCapabilities)) { | ||
|  |                 this.supportsMarkdown = true; | ||
|  |                 return this.supportsMarkdown; | ||
|  |             } | ||
|  |             const contentFormat = this.lsOptions.clientCapabilities?.textDocument?.hover?.contentFormat; | ||
|  |             this.supportsMarkdown = Array.isArray(contentFormat) && contentFormat.indexOf(MarkupKind.Markdown) !== -1; | ||
|  |         } | ||
|  |         return this.supportsMarkdown; | ||
|  |     } | ||
|  | } | ||
|  | function trimQuotes(s) { | ||
|  |     if (s.length <= 1) { | ||
|  |         return s.replace(/['"]/, ''); | ||
|  |     } | ||
|  |     if (s[0] === `'` || s[0] === `"`) { | ||
|  |         s = s.slice(1); | ||
|  |     } | ||
|  |     if (s[s.length - 1] === `'` || s[s.length - 1] === `"`) { | ||
|  |         s = s.slice(0, -1); | ||
|  |     } | ||
|  |     return s; | ||
|  | } |