144 lines
		
	
	
	
		
			5.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			144 lines
		
	
	
	
		
			5.8 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 * as strings from '../utils/strings';
 | |
| import { URI as Uri } from 'vscode-uri';
 | |
| import { TokenType, Range } from '../htmlLanguageTypes';
 | |
| function normalizeRef(url) {
 | |
|     const first = url[0];
 | |
|     const last = url[url.length - 1];
 | |
|     if (first === last && (first === '\'' || first === '\"')) {
 | |
|         url = url.substring(1, url.length - 1);
 | |
|     }
 | |
|     return url;
 | |
| }
 | |
| function validateRef(url, languageId) {
 | |
|     if (!url.length) {
 | |
|         return false;
 | |
|     }
 | |
|     if (languageId === 'handlebars' && /{{|}}/.test(url)) {
 | |
|         return false;
 | |
|     }
 | |
|     return /\b(w[\w\d+.-]*:\/\/)?[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/?))/.test(url);
 | |
| }
 | |
| function getWorkspaceUrl(documentUri, tokenContent, documentContext, base) {
 | |
|     if (/^\s*javascript\:/i.test(tokenContent) || /[\n\r]/.test(tokenContent)) {
 | |
|         return undefined;
 | |
|     }
 | |
|     tokenContent = tokenContent.replace(/^\s*/g, '');
 | |
|     const match = tokenContent.match(/^(\w[\w\d+.-]*):/);
 | |
|     if (match) {
 | |
|         // Absolute link that needs no treatment
 | |
|         const schema = match[1].toLowerCase();
 | |
|         if (schema === 'http' || schema === 'https' || schema === 'file') {
 | |
|             return tokenContent;
 | |
|         }
 | |
|         return undefined;
 | |
|     }
 | |
|     if (/^\#/i.test(tokenContent)) {
 | |
|         return documentUri + tokenContent;
 | |
|     }
 | |
|     if (/^\/\//i.test(tokenContent)) {
 | |
|         // Absolute link (that does not name the protocol)
 | |
|         const pickedScheme = strings.startsWith(documentUri, 'https://') ? 'https' : 'http';
 | |
|         return pickedScheme + ':' + tokenContent.replace(/^\s*/g, '');
 | |
|     }
 | |
|     if (documentContext) {
 | |
|         return documentContext.resolveReference(tokenContent, base || documentUri);
 | |
|     }
 | |
|     return tokenContent;
 | |
| }
 | |
| function createLink(document, documentContext, attributeValue, startOffset, endOffset, base) {
 | |
|     const tokenContent = normalizeRef(attributeValue);
 | |
|     if (!validateRef(tokenContent, document.languageId)) {
 | |
|         return undefined;
 | |
|     }
 | |
|     if (tokenContent.length < attributeValue.length) {
 | |
|         startOffset++;
 | |
|         endOffset--;
 | |
|     }
 | |
|     const workspaceUrl = getWorkspaceUrl(document.uri, tokenContent, documentContext, base);
 | |
|     if (!workspaceUrl || !isValidURI(workspaceUrl)) {
 | |
|         return undefined;
 | |
|     }
 | |
|     return {
 | |
|         range: Range.create(document.positionAt(startOffset), document.positionAt(endOffset)),
 | |
|         target: workspaceUrl
 | |
|     };
 | |
| }
 | |
| function isValidURI(uri) {
 | |
|     try {
 | |
|         Uri.parse(uri);
 | |
|         return true;
 | |
|     }
 | |
|     catch (e) {
 | |
|         return false;
 | |
|     }
 | |
| }
 | |
| export class HTMLDocumentLinks {
 | |
|     constructor(dataManager) {
 | |
|         this.dataManager = dataManager;
 | |
|     }
 | |
|     findDocumentLinks(document, documentContext) {
 | |
|         const newLinks = [];
 | |
|         const scanner = createScanner(document.getText(), 0);
 | |
|         let token = scanner.scan();
 | |
|         let lastAttributeName = undefined;
 | |
|         let lastTagName = undefined;
 | |
|         let afterBase = false;
 | |
|         let base = void 0;
 | |
|         const idLocations = {};
 | |
|         while (token !== TokenType.EOS) {
 | |
|             switch (token) {
 | |
|                 case TokenType.StartTag:
 | |
|                     lastTagName = scanner.getTokenText().toLowerCase();
 | |
|                     if (!base) {
 | |
|                         afterBase = lastTagName === 'base';
 | |
|                     }
 | |
|                     break;
 | |
|                 case TokenType.AttributeName:
 | |
|                     lastAttributeName = scanner.getTokenText().toLowerCase();
 | |
|                     break;
 | |
|                 case TokenType.AttributeValue:
 | |
|                     if (lastTagName && lastAttributeName && this.dataManager.isPathAttribute(lastTagName, lastAttributeName)) {
 | |
|                         const attributeValue = scanner.getTokenText();
 | |
|                         if (!afterBase) { // don't highlight the base link itself
 | |
|                             const link = createLink(document, documentContext, attributeValue, scanner.getTokenOffset(), scanner.getTokenEnd(), base);
 | |
|                             if (link) {
 | |
|                                 newLinks.push(link);
 | |
|                             }
 | |
|                         }
 | |
|                         if (afterBase && typeof base === 'undefined') {
 | |
|                             base = normalizeRef(attributeValue);
 | |
|                             if (base && documentContext) {
 | |
|                                 base = documentContext.resolveReference(base, document.uri);
 | |
|                             }
 | |
|                         }
 | |
|                         afterBase = false;
 | |
|                         lastAttributeName = undefined;
 | |
|                     }
 | |
|                     else if (lastAttributeName === 'id') {
 | |
|                         const id = normalizeRef(scanner.getTokenText());
 | |
|                         idLocations[id] = scanner.getTokenOffset();
 | |
|                     }
 | |
|                     break;
 | |
|             }
 | |
|             token = scanner.scan();
 | |
|         }
 | |
|         // change local links with ids to actual positions
 | |
|         for (const link of newLinks) {
 | |
|             const localWithHash = document.uri + '#';
 | |
|             if (link.target && strings.startsWith(link.target, localWithHash)) {
 | |
|                 const target = link.target.substring(localWithHash.length);
 | |
|                 const offset = idLocations[target];
 | |
|                 if (offset !== undefined) {
 | |
|                     const pos = document.positionAt(offset);
 | |
|                     link.target = `${localWithHash}${pos.line + 1},${pos.character + 1}`;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         return newLinks;
 | |
|     }
 | |
| }
 | 
