Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/htmlLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export * from './htmlLanguageTypes.js';
export interface LanguageService {
setDataProviders(useDefaultDataProvider: boolean, customDataProviders: IHTMLDataProvider[]): void;
createScanner(input: string, initialOffset?: number): Scanner;
parseHTMLDocument(document: TextDocument): HTMLDocument;
parseHTMLDocument(document: TextDocument, options?: { createNodesForOrphanEndTags?: boolean }): HTMLDocument;
findDocumentHighlights(document: TextDocument, position: Position, htmlDocument: HTMLDocument): DocumentHighlight[];
doComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument, options?: CompletionConfiguration): CompletionList;
doComplete2(document: TextDocument, position: Position, htmlDocument: HTMLDocument, documentContext: DocumentContext, options?: CompletionConfiguration): Promise<CompletionList>;
Expand Down
13 changes: 10 additions & 3 deletions src/parser/htmlParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ export class HTMLParser {

}

public parseDocument(document: TextDocument): HTMLDocument {
return this.parse(document.getText(), this.dataManager.getVoidElements(document.languageId));
public parseDocument(document: TextDocument, options?: { createNodesForOrphanEndTags?: boolean }): HTMLDocument {
return this.parse(document.getText(), this.dataManager.getVoidElements(document.languageId), options);
}

public parse(text: string, voidElements: string[]): HTMLDocument {
public parse(text: string, voidElements: string[], options?: { createNodesForOrphanEndTags?: boolean }): HTMLDocument {
const scanner = createScanner(text, undefined, undefined, true);

const htmlDocument = new Node(0, text.length, [], void 0);
Expand Down Expand Up @@ -137,6 +137,13 @@ export class HTMLParser {
curr.endTagStart = endTagStart;
curr.end = scanner.getTokenEnd();
curr = curr.parent!;
} else if (options?.createNodesForOrphanEndTags === true) {
// closing tag without a matching opening tag (opt-in behavior)
const orphan = new Node(endTagStart, scanner.getTokenEnd(), [], curr);
orphan.tag = endTagName;
orphan.closed = true;
orphan.endTagStart = endTagStart;
curr.children.push(orphan);
}
break;
case TokenType.AttributeName: {
Expand Down
30 changes: 26 additions & 4 deletions src/test/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { HTMLParser, Node } from '../parser/htmlParser.js';
import { HTMLDataManager } from '../languageFacts/dataManager.js';

suite('HTML Parser', () => {
function parse(text: string) {
function parse(text: string, options?: { createNodesForOrphanEndTags?: boolean }) {
const htmlDataManager = new HTMLDataManager({});
return new HTMLParser(htmlDataManager).parse(text, htmlDataManager.getVoidElements('html'));
return new HTMLParser(htmlDataManager).parse(text, htmlDataManager.getVoidElements('html'), options);
}
function toJSON(node: Node): any {
return { tag: node.tag, start: node.start, end: node.end, endTagStart: node.endTagStart, closed: node.closed, children: node.children.map(toJSON) };
Expand All @@ -21,8 +21,8 @@ suite('HTML Parser', () => {
return { tag: node.tag, attributes: node.attributes, children: node.children.map(toJSONWithAttributes) };
}

function assertDocument(input: string, expected: any) {
const document = parse(input);
function assertDocument(input: string, expected: any, options?: { createNodesForOrphanEndTags?: boolean }) {
const document = parse(input, options);
assert.deepEqual(document.roots.map(toJSON), expected);
}

Expand Down Expand Up @@ -66,6 +66,28 @@ suite('HTML Parser', () => {
assertDocument('<h1><div><span></h1>', [{ tag: 'h1', start: 0, end: 20, endTagStart: 15, closed: true, children: [{ tag: 'div', start: 4, end: 15, endTagStart: void 0, closed: false, children: [{ tag: 'span', start: 9, end: 15, endTagStart: void 0, closed: false, children: [] }] }] }]);
});

test('Closing tag without opening tag (issue #149) - opt-in', () => {
const withOrphans = { createNodesForOrphanEndTags: true };
// standalone closing tag
assertDocument('</div>', [{ tag: 'div', start: 0, end: 6, endTagStart: 0, closed: true, children: [] }], withOrphans);
// closing tag for a different element than the one opened
assertDocument('<div></span></div>', [{ tag: 'div', start: 0, end: 18, endTagStart: 12, closed: true, children: [{ tag: 'span', start: 5, end: 12, endTagStart: 5, closed: true, children: [] }] }], withOrphans);
// multiple orphan closing tags
assertDocument('</a></b>', [
{ tag: 'a', start: 0, end: 4, endTagStart: 0, closed: true, children: [] },
{ tag: 'b', start: 4, end: 8, endTagStart: 4, closed: true, children: [] }
], withOrphans);
});

test('Closing tag without opening tag (issue #149) - default discards orphans', () => {
// default (option absent) keeps original behavior: orphan closing tags are discarded
assertDocument('</div>', []);
assertDocument('</a></b>', []);
assertDocument('<div></span></div>', [{ tag: 'div', start: 0, end: 18, endTagStart: 12, closed: true, children: [] }]);
// explicitly false behaves the same as absent
assertDocument('</div>', [], { createNodesForOrphanEndTags: false });
});

test('MissingBrackets', () => {
assertDocument('<div><div</div>', [{ tag: 'div', start: 0, end: 15, endTagStart: 9, closed: true, children: [{ tag: 'div', start: 5, end: 9, endTagStart: void 0, closed: false, children: [] }] }]);
assertDocument('<div><div\n</div>', [{ tag: 'div', start: 0, end: 16, endTagStart: 10, closed: true, children: [{ tag: 'div', start: 5, end: 10, endTagStart: void 0, closed: false, children: [] }] }]);
Expand Down