1import { Writable, finished as finishedCb, type Readable } from 'node:stream'; 2import * as assert from 'node:assert'; 3import { type TreeAdapter, type Token, defaultTreeAdapter } from 'parse5'; 4import { adapter as htmlparser2Adapter } from 'parse5-htmlparser2-tree-adapter'; 5 6// Ensure the default tree adapter matches the expected type. 7export const treeAdapters = { 8 default: defaultTreeAdapter, 9 htmlparser2: htmlparser2Adapter, 10} as const; 11 12export function addSlashes(str: string): string { 13 return str 14 .replace(/\t/g, '\\t') 15 .replace(/\n/g, '\\n') 16 .replace(/\f/g, '\\f') 17 .replace(/\r/g, '\\r') 18 .replace(/\0/g, '\\u0000'); 19} 20 21function createDiffMarker(markerPosition: number): string { 22 return '^\n'.padStart(markerPosition + 1, ' '); 23} 24 25function getRandomChunkSize(min = 1, max = 10): number { 26 return min + Math.floor(Math.random() * (max - min + 1)); 27} 28 29export function makeChunks(str: string, minSize?: number, maxSize?: number): string[] { 30 if (str.length === 0) { 31 return ['']; 32 } 33 34 const chunks = []; 35 let start = 0; 36 37 // NOTE: start with 1, so we avoid situation when we have just one huge chunk 38 let end = 1; 39 40 while (start < str.length) { 41 chunks.push(str.substring(start, end)); 42 start = end; 43 end = Math.min(end + getRandomChunkSize(minSize, maxSize), str.length); 44 } 45 46 return chunks; 47} 48 49export class WritableStreamStub extends Writable { 50 writtenData = ''; 51 52 constructor() { 53 super({ decodeStrings: false }); 54 } 55 56 override _write(chunk: string, _encoding: string, callback: () => void): void { 57 assert.strictEqual(typeof chunk, 'string', 'Expected output to be a string stream'); 58 this.writtenData += chunk; 59 callback(); 60 } 61} 62 63export function normalizeNewLine(str: string): string { 64 return str.replace(/\r\n/g, '\n'); 65} 66 67export function removeNewLines(str: string): string { 68 return str.replace(/\r/g, '').replace(/\n/g, ''); 69} 70 71export function writeChunkedToStream(str: string, stream: Writable): void { 72 const chunks = makeChunks(str); 73 const lastChunkIdx = chunks.length - 1; 74 75 for (const [idx, chunk] of chunks.entries()) { 76 if (idx === lastChunkIdx) { 77 stream.end(chunk); 78 } else { 79 stream.write(chunk); 80 } 81 } 82} 83 84export function generateTestsForEachTreeAdapter(name: string, ctor: (adapter: TreeAdapter) => void): void { 85 describe(name, () => { 86 for (const adapterName of Object.keys(treeAdapters)) { 87 const adapter = treeAdapters[adapterName as keyof typeof treeAdapters] as TreeAdapter; 88 89 describe(`Tree adapter: ${adapterName}`, () => { 90 ctor(adapter); 91 }); 92 } 93 }); 94} 95 96export function getStringDiffMsg(actual: string, expected: string): string { 97 for (let i = 0; i < expected.length; i++) { 98 if (actual[i] !== expected[i]) { 99 let diffMsg = `\nString differ at index ${i}\n`; 100 101 const expectedStr = `Expected: ${addSlashes(expected.substring(i - 100, i + 1))}`; 102 const expectedDiffMarker = createDiffMarker(expectedStr.length); 103 104 diffMsg += `${expectedStr}${addSlashes(expected.substring(i + 1, i + 20))}\n${expectedDiffMarker}`; 105 106 const actualStr = `Actual: ${addSlashes(actual.substring(i - 100, i + 1))}`; 107 const actualDiffMarker = createDiffMarker(actualStr.length); 108 109 diffMsg += `${actualStr}${addSlashes(actual.substring(i + 1, i + 20))}\n${actualDiffMarker}`; 110 111 return diffMsg; 112 } 113 } 114 115 return ''; 116} 117 118export function getSubstringByLineCol(lines: string[], loc: Token.Location): string { 119 lines = lines.slice(loc.startLine - 1, loc.endLine); 120 121 const last = lines.length - 1; 122 123 lines[last] = lines[last].substring(0, loc.endCol - 1); 124 lines[0] = lines[0].substring(loc.startCol - 1); 125 126 return lines.join('\n'); 127} 128 129// TODO [engine:node@>=16]: Replace this with `finished` from 'node:stream/promises'. 130 131export function finished(stream: Writable | Readable): Promise<void> { 132 return new Promise((resolve, reject) => finishedCb(stream, (err) => (err ? reject(err) : resolve()))); 133} 134