Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

URL autocompletion doesn't work for cite attributes in HTML #159

Merged
merged 3 commits into from
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/htmlLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { HTMLParser } from './parser/htmlParser';
import { HTMLCompletion } from './services/htmlCompletion';
import { HTMLHover } from './services/htmlHover';
import { format } from './services/htmlFormatter';
import { findDocumentLinks } from './services/htmlLinks';
import { HTMLDocumentLinks } from './services/htmlLinks';
import { findDocumentHighlights } from './services/htmlHighlighting';
import { findDocumentSymbols } from './services/htmlSymbolsProvider';
import { doRename } from './services/htmlRename';
Expand Down Expand Up @@ -60,6 +60,7 @@ export function getLanguageService(options: LanguageServiceOptions = defaultLang
const htmlParser = new HTMLParser(dataManager);
const htmlSelectionRange = new HTMLSelectionRange(htmlParser);
const htmlFolding = new HTMLFolding(dataManager);
const htmlDocumentLinks = new HTMLDocumentLinks(dataManager);

return {
setDataProviders: dataManager.setDataProviders.bind(dataManager),
Expand All @@ -71,7 +72,7 @@ export function getLanguageService(options: LanguageServiceOptions = defaultLang
doHover: htmlHover.doHover.bind(htmlHover),
format,
findDocumentHighlights,
findDocumentLinks,
findDocumentLinks: htmlDocumentLinks.findDocumentLinks.bind(htmlDocumentLinks),
findDocumentSymbols,
getFoldingRanges: htmlFolding.getFoldingRanges.bind(htmlFolding),
getSelectionRanges: htmlSelectionRange.getSelectionRanges.bind(htmlSelectionRange),
Expand Down
49 changes: 47 additions & 2 deletions src/languageFacts/dataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,59 @@ export class HTMLDataManager {
return !!e && arrays.binarySearch(voidElements, e.toLowerCase(), (s1: string, s2: string) => s1.localeCompare(s2)) >= 0;
}

getVoidElements(languageId: string):string[];
getVoidElements(languageId: string): string[];
getVoidElements(dataProviders: IHTMLDataProvider[]): string[];
getVoidElements(languageOrProviders: string| IHTMLDataProvider[]): string[] {
getVoidElements(languageOrProviders: string | IHTMLDataProvider[]): string[] {
const dataProviders = Array.isArray(languageOrProviders) ? languageOrProviders : this.getDataProviders().filter(p => p.isApplicable(languageOrProviders!));
const voidTags: string[] = [];
dataProviders.forEach((provider) => {
provider.provideTags().filter(tag => tag.void).forEach(tag => voidTags.push(tag.name));
});
return voidTags.sort();
}

isPathAttribute(tag: string, attr: string) {
// should eventually come from custom data

if (attr === 'src' || attr === 'href') {
return true;
}
const a = PATH_TAG_AND_ATTR[tag];
if (a) {
if (typeof a === 'string') {
return a === attr;
} else {
return a.indexOf(attr) !== -1;
}
}
return false;
}
}

// Selected from https://stackoverflow.com/a/2725168/1780148
const PATH_TAG_AND_ATTR: { [tag: string]: string | string[] } = {
// HTML 4
a: 'href',
area: 'href',
body: 'background',
blockquote: 'cite',
del: 'cite',
form: 'action',
frame: ['src', 'longdesc'],
img: ['src', 'longdesc'],
ins: 'cite',
link: 'href',
object: 'data',
q: 'cite',
script: 'src',
// HTML 5
audio: 'src',
button: 'formaction',
command: 'icon',
embed: 'src',
html: 'manifest',
input: ['src', 'formaction'],
source: 'src',
track: 'src',
video: ['src', 'poster']
};
4 changes: 2 additions & 2 deletions src/services/htmlCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class HTMLCompletion {
return this.doComplete(document, position, htmlDocument, settings);
}

const participant: PathCompletionParticipant = new PathCompletionParticipant(this.lsOptions.fileSystemProvider.readDirectory);
const participant: PathCompletionParticipant = new PathCompletionParticipant(this.dataManager, this.lsOptions.fileSystemProvider.readDirectory);
const contributedParticipants = this.completionParticipants;
this.completionParticipants = [participant as ICompletionParticipant].concat(contributedParticipants);

Expand Down Expand Up @@ -508,7 +508,7 @@ export class HTMLCompletion {
const scanner = createScanner(document.getText(), node.start);
let token = scanner.scan();
while (token !== TokenType.EOS && scanner.getTokenEnd() <= offset) {
if (token === TokenType.AttributeName&& scanner.getTokenEnd() === offset - 1) {
if (token === TokenType.AttributeName && scanner.getTokenEnd() === offset - 1) {
// Ensure the token is a valid standalone attribute name
token = scanner.scan(); // this should be the = just written
if (token !== TokenType.DelimiterAssign) {
Expand Down
113 changes: 61 additions & 52 deletions src/services/htmlLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import * as strings from '../utils/strings';
import { URI as Uri } from 'vscode-uri';

import { TokenType, DocumentContext, TextDocument, Range, DocumentLink } from '../htmlLanguageTypes';
import { HTMLDataManager } from '../languageFacts/dataManager';

function normalizeRef(url: string): string {
const first = url[0];
const last = url[url.length - 1];
if (first === last && (first === '\'' || first === '\"')) {
url = url.substr(1, url.length - 2);
url = url.substring(1, url.length - 1);
}
return url;
}
Expand Down Expand Up @@ -85,63 +86,71 @@ function isValidURI(uri: string) {
}
}

export function findDocumentLinks(document: TextDocument, documentContext: DocumentContext): DocumentLink[] {
const newLinks: DocumentLink[] = [];
export class HTMLDocumentLinks {

const scanner = createScanner(document.getText(), 0);
let token = scanner.scan();
let lastAttributeName: string | undefined = undefined;
let afterBase = false;
let base: string | undefined = void 0;
const idLocations: { [id: string]: number | undefined } = {};
constructor(private dataManager: HTMLDataManager) {
}

while (token !== TokenType.EOS) {
switch (token) {
case TokenType.StartTag:
if (!base) {
const tagName = scanner.getTokenText().toLowerCase();
afterBase = tagName === 'base';
}
break;
case TokenType.AttributeName:
lastAttributeName = scanner.getTokenText().toLowerCase();
break;
case TokenType.AttributeValue:
if (lastAttributeName === 'src' || lastAttributeName === 'href') {
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);
}
findDocumentLinks(document: TextDocument, documentContext: DocumentContext): DocumentLink[] {
const newLinks: DocumentLink[] = [];

const scanner = createScanner(document.getText(), 0);
let token = scanner.scan();
let lastAttributeName: string | undefined = undefined;
let lastTagName: string | undefined = undefined;
let afterBase = false;
let base: string | undefined = void 0;
const idLocations: { [id: string]: number | undefined } = {};

while (token !== TokenType.EOS) {
switch (token) {
case TokenType.StartTag:
lastTagName = scanner.getTokenText().toLowerCase();
if (!base) {
afterBase = lastTagName === 'base';
}
if (afterBase && typeof base === 'undefined') {
base = normalizeRef(attributeValue);
if (base && documentContext) {
base = documentContext.resolveReference(base, document.uri);
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();
}
afterBase = false;
lastAttributeName = undefined;
} else if (lastAttributeName === 'id') {
const id = normalizeRef(scanner.getTokenText());
idLocations[id] = scanner.getTokenOffset();
}
break;
break;
}
token = scanner.scan();
}
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.substr(localWithHash.length);
const offset = idLocations[target];
if (offset !== undefined) {
const pos = document.positionAt(offset);
link.target = `${localWithHash}${pos.line + 1},${pos.character + 1}`;
// 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;
}
return newLinks;
}
}

46 changes: 3 additions & 43 deletions src/services/pathCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
*--------------------------------------------------------------------------------------------*/

import { ICompletionParticipant, TextDocument, CompletionItemKind, CompletionItem, TextEdit, Range, Position, DocumentUri, FileType, HtmlAttributeValueContext, DocumentContext, CompletionList } from '../htmlLanguageTypes';
import { HTMLDataManager } from '../languageFacts/dataManager';
import { startsWith } from '../utils/strings';

export class PathCompletionParticipant implements ICompletionParticipant {
private atributeCompletions: HtmlAttributeValueContext[] = [];

constructor(private readonly readDirectory: (uri: DocumentUri) => Promise<[string, FileType][]>) {
constructor(private dataManager: HTMLDataManager, private readonly readDirectory: (uri: DocumentUri) => Promise<[string, FileType][]>) {
}

public onHtmlAttributeValue(context: HtmlAttributeValueContext) {
if (isPathAttribute(context.tag, context.attribute)) {
if (this.dataManager.isPathAttribute(context.tag, context.attribute)) {
this.atributeCompletions.push(context);
}
}
Expand Down Expand Up @@ -78,21 +79,6 @@ function isCompletablePath(value: string) {
return true;
}

function isPathAttribute(tag: string, attr: string) {
if (attr === 'src' || attr === 'href') {
return true;
}
const a = PATH_TAG_AND_ATTR[tag];
if (a) {
if (typeof a === 'string') {
return a === attr;
} else {
return a.indexOf(attr) !== -1;
}
}
return false;
}

function pathToReplaceRange(valueBeforeCursor: string, fullValue: string, range: Range) {
let replaceRange: Range;
const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/');
Expand Down Expand Up @@ -148,29 +134,3 @@ function shiftRange(range: Range, startOffset: number, endOffset: number): Range
return Range.create(start, end);
}

// Selected from https://stackoverflow.com/a/2725168/1780148
const PATH_TAG_AND_ATTR: { [tag: string]: string | string[] } = {
// HTML 4
a: 'href',
area: 'href',
body: 'background',
del: 'cite',
form: 'action',
frame: ['src', 'longdesc'],
img: ['src', 'longdesc'],
ins: 'cite',
link: 'href',
object: 'data',
q: 'cite',
script: 'src',
// HTML 5
audio: 'src',
button: 'formaction',
command: 'icon',
embed: 'src',
html: 'manifest',
input: ['src', 'formaction'],
source: 'src',
track: 'src',
video: ['src', 'poster']
};
1 change: 1 addition & 0 deletions src/test/links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ suite('HTML Link Detection', () => {
testLinkDetection('<a href="mailto:<%- mail %>@<%- domain %>" > <% - mail %>@<% - domain %> </a>', []);

testLinkDetection('<link rel="icon" type="image/x-icon" href="data:@file/x-icon;base64,AAABAAIAQEAAAAEAIAAoQgAAJgA">', []);
testLinkDetection('<blockquote cite="foo.png">', [{ offset: 18, length: 7, target: 'file:///test/data/abc/foo.png' }]);
});

test('Local targets', () => {
Expand Down