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