• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import * as assert from 'node:assert';
2import * as fs from 'node:fs';
3import * as path from 'node:path';
4import { type TreeAdapterTypeMap, type TreeAdapter, type ParserOptions, type Token, serializeOuter } from 'parse5';
5import {
6    removeNewLines,
7    getSubstringByLineCol,
8    getStringDiffMsg,
9    normalizeNewLine,
10    generateTestsForEachTreeAdapter,
11} from './common.js';
12import { serializeDoctypeContent } from 'parse5-htmlparser2-tree-adapter';
13
14function walkTree<T extends TreeAdapterTypeMap>(
15    parent: T['parentNode'],
16    treeAdapter: TreeAdapter<T>,
17    handler: (node: T['node']) => void
18): void {
19    for (const node of treeAdapter.getChildNodes(parent)) {
20        if (treeAdapter.isElementNode(node)) {
21            walkTree(node, treeAdapter, handler);
22        }
23
24        handler(node);
25    }
26}
27
28function assertLocation(loc: Token.Location, expected: string, html: string, lines: string[]): void {
29    //Offsets
30    let actual = html.substring(loc.startOffset, loc.endOffset);
31
32    expected = removeNewLines(expected);
33    actual = removeNewLines(actual);
34
35    assert.ok(expected === actual, getStringDiffMsg(actual, expected));
36
37    //Line/col
38    actual = getSubstringByLineCol(lines, loc);
39    actual = removeNewLines(actual);
40
41    assert.ok(actual === expected, getStringDiffMsg(actual, expected));
42}
43
44//NOTE: Based on the idea that the serialized fragment starts with the startTag
45export function assertStartTagLocation(
46    location: Token.ElementLocation,
47    serializedNode: string,
48    html: string,
49    lines: string[]
50): void {
51    assert.ok(location.startTag, 'Expected startTag to be defined');
52    const length = location.startTag.endOffset - location.startTag.startOffset;
53    const expected = serializedNode.substring(0, length);
54
55    assertLocation(location.startTag, expected, html, lines);
56}
57
58//NOTE: Based on the idea that the serialized fragment ends with the endTag
59function assertEndTagLocation(
60    location: Token.ElementLocation,
61    serializedNode: string,
62    html: string,
63    lines: string[]
64): void {
65    assert.ok(location.endTag, 'Expected endTag to be defined');
66    const length = location.endTag.endOffset - location.endTag.startOffset;
67    const expected = serializedNode.slice(-length);
68
69    assertLocation(location.endTag, expected, html, lines);
70}
71
72function assertAttrsLocation(
73    location: Token.ElementLocation,
74    serializedNode: string,
75    html: string,
76    lines: string[]
77): void {
78    assert.ok(location.attrs, 'Expected attrs to be defined');
79
80    for (const attr of Object.values(location.attrs)) {
81        const expected = serializedNode.slice(
82            attr.startOffset - location.startOffset,
83            attr.endOffset - location.startOffset
84        );
85
86        assertLocation(attr, expected, html, lines);
87    }
88}
89
90export function assertNodeLocation(
91    location: Token.Location,
92    serializedNode: string,
93    html: string,
94    lines: string[]
95): void {
96    const expected = removeNewLines(serializedNode);
97
98    assertLocation(location, expected, html, lines);
99}
100
101function loadParserLocationInfoTestData(): { name: string; data: string }[] {
102    const dataDirPath = new URL('../data/location-info', import.meta.url);
103    const testSetFileDirs = fs.readdirSync(dataDirPath);
104
105    return testSetFileDirs.map((dirName) => {
106        const dataFilePath = path.join(dataDirPath.pathname, dirName, 'data.html');
107        const data = fs.readFileSync(dataFilePath).toString();
108
109        return {
110            name: dirName,
111            data: normalizeNewLine(data),
112        };
113    });
114}
115
116export function generateLocationInfoParserTests(
117    name: string,
118    parse: (html: string, opts: ParserOptions<TreeAdapterTypeMap>) => { node: TreeAdapterTypeMap['node'] }
119): void {
120    generateTestsForEachTreeAdapter(name, (treeAdapter) => {
121        for (const test of loadParserLocationInfoTestData()) {
122            //NOTE: How it works: we parse document with location info.
123            //Then for each node in the tree we run the serializer and compare results with the substring
124            //obtained via the location info from the expected serialization results.
125            it(`Location info (Parser) - ${test.name}`, async () => {
126                const html = test.data;
127                const lines = html.split(/\r?\n/g);
128
129                const parserOpts = {
130                    treeAdapter,
131                    sourceCodeLocationInfo: true,
132                };
133
134                const parsingResult = parse(html, parserOpts);
135                const document = parsingResult.node;
136
137                walkTree(document, treeAdapter, (node) => {
138                    const location = treeAdapter.getNodeSourceCodeLocation(node);
139
140                    assert.ok(location);
141
142                    const serializedNode = treeAdapter.isDocumentTypeNode(node)
143                        ? `<${serializeDoctypeContent(
144                              treeAdapter.getDocumentTypeNodeName(node),
145                              treeAdapter.getDocumentTypeNodePublicId(node),
146                              treeAdapter.getDocumentTypeNodeSystemId(node)
147                          )}>`
148                        : serializeOuter(node, { treeAdapter });
149
150                    assertLocation(location, serializedNode, html, lines);
151
152                    if (treeAdapter.isElementNode(node)) {
153                        assertStartTagLocation(location, serializedNode, html, lines);
154
155                        if (location.endTag) {
156                            assertEndTagLocation(location, serializedNode, html, lines);
157                        }
158
159                        if (location.attrs) {
160                            assertAttrsLocation(location, serializedNode, html, lines);
161                        } else {
162                            // If we don't have `location.attrs`, we expect that the node has no attributes.
163                            assert.strictEqual(treeAdapter.getAttrList(node).length, 0);
164                        }
165                    }
166                });
167            });
168        }
169    });
170}
171