• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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