/*--------------------------------------------------------------------------------------------- * 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) { return undefined; } const target = validateAndCleanURI(workspaceUrl); return { range: Range.create(document.positionAt(startOffset), document.positionAt(endOffset)), target }; } function validateAndCleanURI(uriStr) { try { const uri = Uri.parse(uriStr); if (uri.query) { return uri.with({ query: null }).toString(/* skipEncodig*/ true); } return uriStr; } catch (e) { return undefined; } } 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; } }