mirror of
https://github.com/zrwusa/data-structure-typed.git
synced 2024-11-23 04:44:04 +00:00
chore: Implement unit tests as example code in the README.md and source code comments.
This commit is contained in:
parent
476395ef72
commit
0ff5ddc410
|
@ -8,7 +8,7 @@ All notable changes to this project will be documented in this file.
|
|||
- [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
||||
- [`auto-changelog`](https://github.com/CookPete/auto-changelog)
|
||||
|
||||
## [v1.53.1](https://github.com/zrwusa/data-structure-typed/compare/v1.51.5...main) (upcoming)
|
||||
## [v1.53.2](https://github.com/zrwusa/data-structure-typed/compare/v1.51.5...main) (upcoming)
|
||||
|
||||
### Changes
|
||||
|
||||
|
|
|
@ -798,6 +798,12 @@ Array.from(dijkstraResult?.seen ?? []).map(vertex => vertex.key) // ['A', 'B', '
|
|||
|
||||
```
|
||||
|
||||
|
||||
[//]: # (No deletion!!! Start of Example Replace Section)
|
||||
|
||||
|
||||
[//]: # (No deletion!!! End of Example Replace Section)
|
||||
|
||||
## API docs & Examples
|
||||
|
||||
[API Docs](https://data-structure-typed-docs.vercel.app)
|
||||
|
|
52
package-lock.json
generated
52
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "data-structure-typed",
|
||||
"version": "1.53.1",
|
||||
"version": "1.53.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "data-structure-typed",
|
||||
"version": "1.53.1",
|
||||
"version": "1.53.2",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.2",
|
||||
|
@ -19,11 +19,11 @@
|
|||
"@typescript-eslint/eslint-plugin": "^8.12.1",
|
||||
"@typescript-eslint/parser": "^8.12.1",
|
||||
"auto-changelog": "^2.5.0",
|
||||
"avl-tree-typed": "^1.53.0",
|
||||
"avl-tree-typed": "^1.53.1",
|
||||
"benchmark": "^2.1.4",
|
||||
"binary-tree-typed": "^1.53.0",
|
||||
"bst-typed": "^1.53.0",
|
||||
"data-structure-typed": "^1.53.0",
|
||||
"binary-tree-typed": "^1.53.1",
|
||||
"bst-typed": "^1.53.1",
|
||||
"data-structure-typed": "^1.53.1",
|
||||
"dependency-cruiser": "^16.5.0",
|
||||
"doctoc": "^2.2.1",
|
||||
"eslint": "^9.13.0",
|
||||
|
@ -32,7 +32,7 @@
|
|||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"heap-typed": "^1.53.0",
|
||||
"heap-typed": "^1.53.1",
|
||||
"istanbul-badges-readme": "^1.9.0",
|
||||
"jest": "^29.7.0",
|
||||
"js-sdsl": "^4.4.2",
|
||||
|
@ -3437,13 +3437,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/avl-tree-typed": {
|
||||
"version": "1.53.0",
|
||||
"resolved": "https://registry.npmjs.org/avl-tree-typed/-/avl-tree-typed-1.53.0.tgz",
|
||||
"integrity": "sha512-SVk6zEvzrB2GhHVFOhVO2yaB7p9iLGy2aml0j7UW1rWp+VACw4SZQhKuldl2qC7ECb9AOIbajD7Y9ott8Qfy8Q==",
|
||||
"version": "1.53.1",
|
||||
"resolved": "https://registry.npmjs.org/avl-tree-typed/-/avl-tree-typed-1.53.1.tgz",
|
||||
"integrity": "sha512-4k54zM7lNtz37FSgNstoGlRSqN9kO1y98Lz7+8RXAoOtYe1McIUyJYhuukOFJi6XshEQDFHaxz/zklZJFqLq/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-structure-typed": "^1.53.0"
|
||||
"data-structure-typed": "^1.53.1"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
|
@ -3602,13 +3602,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/binary-tree-typed": {
|
||||
"version": "1.53.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-tree-typed/-/binary-tree-typed-1.53.0.tgz",
|
||||
"integrity": "sha512-5EmlwbR4Kaphual+wF2rJwpZFhhK2u6zOFiA+yagWDbH6tzBCojT5o2DN+1tmW1Pj6bGJxz7PHcDBIdBkQruMw==",
|
||||
"version": "1.53.1",
|
||||
"resolved": "https://registry.npmjs.org/binary-tree-typed/-/binary-tree-typed-1.53.1.tgz",
|
||||
"integrity": "sha512-Ym/VfNG8iRhBUxxlCFArQSbf46m39voLVXdKbLzP/G3Sb3N7xwSwMTRC2Evx9lEWnhMmg4WHXsqZByun2xIljw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-structure-typed": "^1.53.0"
|
||||
"data-structure-typed": "^1.53.1"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
|
@ -3691,13 +3691,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/bst-typed": {
|
||||
"version": "1.53.0",
|
||||
"resolved": "https://registry.npmjs.org/bst-typed/-/bst-typed-1.53.0.tgz",
|
||||
"integrity": "sha512-f1f+RlbIA6J2reFO/DwLRLaZVBmMNNyKCi5eWd+r5ojV9bDC/IZ34nzID0zSJUwVqtAtIpaK9x63XAo42+5SoQ==",
|
||||
"version": "1.53.1",
|
||||
"resolved": "https://registry.npmjs.org/bst-typed/-/bst-typed-1.53.1.tgz",
|
||||
"integrity": "sha512-pSAIStgqneF4kTbbJJU7tcTMUBvchUXoID9fpKmcpsLHzG0jgfmMnGYZBZlRQXYJkUAsiFHeDaDMbK6W2muBQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-structure-typed": "^1.53.0"
|
||||
"data-structure-typed": "^1.53.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
|
@ -4069,9 +4069,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/data-structure-typed": {
|
||||
"version": "1.53.0",
|
||||
"resolved": "https://registry.npmjs.org/data-structure-typed/-/data-structure-typed-1.53.0.tgz",
|
||||
"integrity": "sha512-bukacSwbWyypNTdQCt51rwNcqM1hacHG6mMJ2RDBqsz8jJ5N4oMOgQhDyA1GkvMvdq83mcj9ilCFZ6lDcwShqA==",
|
||||
"version": "1.53.1",
|
||||
"resolved": "https://registry.npmjs.org/data-structure-typed/-/data-structure-typed-1.53.1.tgz",
|
||||
"integrity": "sha512-D3KDDnf6zY7vW45ht/U467UCVx1Zs6E3uVlGe4HSa+qvMHtPZMZahU5X8HbQVfzHZ1wg7NBKW6OtQdy0YzbuSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
@ -5946,13 +5946,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/heap-typed": {
|
||||
"version": "1.53.0",
|
||||
"resolved": "https://registry.npmjs.org/heap-typed/-/heap-typed-1.53.0.tgz",
|
||||
"integrity": "sha512-MtbVdNGb2mvLrv67j0ajaMDr/u0HpR0tMaCj1MUrmXKK+IhVqniq7I8ADUlDrEPpKXwES8RBa0v7lA5umKR9hg==",
|
||||
"version": "1.53.1",
|
||||
"resolved": "https://registry.npmjs.org/heap-typed/-/heap-typed-1.53.1.tgz",
|
||||
"integrity": "sha512-4/fHFXV3YLC0wvi+H3IXTRF4t2CzU1AcaB8PZvU/vR5V7lTPHNAb9IUGj9bC1f/7tmz/3a38r1Mx0xJVPIhzaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-structure-typed": "^1.53.0"
|
||||
"data-structure-typed": "^1.53.1"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
|
|
10
package.json
10
package.json
|
@ -69,11 +69,11 @@
|
|||
"@typescript-eslint/eslint-plugin": "^8.12.1",
|
||||
"@typescript-eslint/parser": "^8.12.1",
|
||||
"auto-changelog": "^2.5.0",
|
||||
"avl-tree-typed": "^1.53.0",
|
||||
"avl-tree-typed": "^1.53.1",
|
||||
"benchmark": "^2.1.4",
|
||||
"binary-tree-typed": "^1.53.0",
|
||||
"bst-typed": "^1.53.0",
|
||||
"data-structure-typed": "^1.53.0",
|
||||
"binary-tree-typed": "^1.53.1",
|
||||
"bst-typed": "^1.53.1",
|
||||
"data-structure-typed": "^1.53.1",
|
||||
"dependency-cruiser": "^16.5.0",
|
||||
"doctoc": "^2.2.1",
|
||||
"eslint": "^9.13.0",
|
||||
|
@ -82,7 +82,7 @@
|
|||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"heap-typed": "^1.53.0",
|
||||
"heap-typed": "^1.53.1",
|
||||
"istanbul-badges-readme": "^1.9.0",
|
||||
"jest": "^29.7.0",
|
||||
"js-sdsl": "^4.4.2",
|
||||
|
|
|
@ -457,3 +457,178 @@ describe('FibonacciHeap Stress Test', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('classic use', () => {
|
||||
it('heap sort', () => {
|
||||
function heapSort(arr: number[]): number[] {
|
||||
const heap = new Heap<number>(arr, { comparator: (a, b) => a - b });
|
||||
const sorted: number[] = [];
|
||||
while (!heap.isEmpty()) {
|
||||
sorted.push(heap.poll()!); // Poll minimum element
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
const array = [5, 3, 8, 4, 1, 2];
|
||||
expect(heapSort(array)).toEqual([1, 2, 3, 4, 5, 8]);
|
||||
});
|
||||
|
||||
it('top k', () => {
|
||||
function topKElements(arr: number[], k: number): number[] {
|
||||
const heap = new Heap<number>([], { comparator: (a, b) => b - a }); // Max heap
|
||||
arr.forEach(num => {
|
||||
heap.add(num);
|
||||
if (heap.size > k) heap.poll(); // Keep the heap size at K
|
||||
});
|
||||
return heap.toArray();
|
||||
}
|
||||
|
||||
const numbers = [10, 30, 20, 5, 15, 25];
|
||||
console.log('Top K:', topKElements(numbers, 3)); // [15, 10, 5]
|
||||
expect(topKElements(numbers, 3)).toEqual([15, 10, 5]);
|
||||
});
|
||||
|
||||
it('merge sorted sequences', () => {
|
||||
function mergeSortedSequences(sequences: number[][]): number[] {
|
||||
const heap = new Heap<{ value: number; seqIndex: number; itemIndex: number }>([], {
|
||||
comparator: (a, b) => a.value - b.value // Min heap
|
||||
});
|
||||
|
||||
// Initialize heap
|
||||
sequences.forEach((seq, seqIndex) => {
|
||||
if (seq.length) {
|
||||
heap.add({ value: seq[0], seqIndex, itemIndex: 0 });
|
||||
}
|
||||
});
|
||||
|
||||
const merged: number[] = [];
|
||||
while (!heap.isEmpty()) {
|
||||
const { value, seqIndex, itemIndex } = heap.poll()!;
|
||||
merged.push(value);
|
||||
|
||||
if (itemIndex + 1 < sequences[seqIndex].length) {
|
||||
heap.add({
|
||||
value: sequences[seqIndex][itemIndex + 1],
|
||||
seqIndex,
|
||||
itemIndex: itemIndex + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
const sequences = [
|
||||
[1, 4, 7],
|
||||
[2, 5, 8],
|
||||
[3, 6, 9]
|
||||
];
|
||||
console.log('Merged Sequences:', mergeSortedSequences(sequences)); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
expect(mergeSortedSequences(sequences)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
});
|
||||
|
||||
it('@example median finder', () => {
|
||||
class MedianFinder {
|
||||
private low: MaxHeap<number>; // Max heap, stores the smaller half
|
||||
private high: MinHeap<number>; // Min heap, stores the larger half
|
||||
|
||||
constructor() {
|
||||
this.low = new MaxHeap<number>([]);
|
||||
this.high = new MinHeap<number>([]);
|
||||
}
|
||||
|
||||
addNum(num: number): void {
|
||||
if (this.low.isEmpty() || num <= this.low.peek()!) this.low.add(num);
|
||||
else this.high.add(num);
|
||||
|
||||
// Balance heaps
|
||||
if (this.low.size > this.high.size + 1) this.high.add(this.low.poll()!);
|
||||
else if (this.high.size > this.low.size) this.low.add(this.high.poll()!);
|
||||
}
|
||||
|
||||
findMedian(): number {
|
||||
return this.low.peek()!;
|
||||
}
|
||||
}
|
||||
|
||||
const medianFinder = new MedianFinder();
|
||||
medianFinder.addNum(10);
|
||||
expect(medianFinder.findMedian()).toBe(10);
|
||||
medianFinder.addNum(20);
|
||||
expect(medianFinder.findMedian()).toBe(10);
|
||||
medianFinder.addNum(30);
|
||||
expect(medianFinder.findMedian()).toBe(20);
|
||||
medianFinder.addNum(40);
|
||||
expect(medianFinder.findMedian()).toBe(20);
|
||||
medianFinder.addNum(50);
|
||||
expect(medianFinder.findMedian()).toBe(30);
|
||||
});
|
||||
|
||||
it('schedule tasks', () => {
|
||||
type Task = [string, number];
|
||||
|
||||
function scheduleTasks(tasks: Task[], machines: number): Map<number, Task[]> {
|
||||
const machineHeap = new Heap<{ id: number; load: number }>([], { comparator: (a, b) => a.load - b.load }); // Min heap
|
||||
const allocation = new Map<number, Task[]>();
|
||||
|
||||
// Initialize the load on each machine
|
||||
for (let i = 0; i < machines; i++) {
|
||||
machineHeap.add({ id: i, load: 0 });
|
||||
allocation.set(i, []);
|
||||
}
|
||||
|
||||
// Assign tasks
|
||||
tasks.forEach(([task, load]) => {
|
||||
const machine = machineHeap.poll()!;
|
||||
allocation.get(machine.id)!.push([task, load]);
|
||||
machine.load += load;
|
||||
machineHeap.add(machine); // The machine after updating the load is re-entered into the heap
|
||||
});
|
||||
|
||||
return allocation;
|
||||
}
|
||||
|
||||
const tasks: Task[] = [
|
||||
['Task1', 3],
|
||||
['Task2', 1],
|
||||
['Task3', 2],
|
||||
['Task4', 5],
|
||||
['Task5', 4]
|
||||
];
|
||||
const expectedMap = new Map<number, Task[]>();
|
||||
expectedMap.set(0, [
|
||||
['Task1', 3],
|
||||
['Task4', 5]
|
||||
]);
|
||||
expectedMap.set(1, [
|
||||
['Task2', 1],
|
||||
['Task3', 2],
|
||||
['Task5', 4]
|
||||
]);
|
||||
expect(scheduleTasks(tasks, 2)).toEqual(expectedMap);
|
||||
});
|
||||
|
||||
it('@example Use Heap for load balancing', () => {
|
||||
function loadBalance(requests: number[], servers: number): number[] {
|
||||
const serverHeap = new Heap<{ id: number; load: number }>([], { comparator: (a, b) => a.load - b.load }); // min heap
|
||||
const serverLoads = new Array(servers).fill(0);
|
||||
|
||||
for (let i = 0; i < servers; i++) {
|
||||
serverHeap.add({ id: i, load: 0 });
|
||||
}
|
||||
|
||||
requests.forEach(req => {
|
||||
const server = serverHeap.poll()!;
|
||||
serverLoads[server.id] += req;
|
||||
server.load += req;
|
||||
serverHeap.add(server); // The server after updating the load is re-entered into the heap
|
||||
});
|
||||
|
||||
return serverLoads;
|
||||
}
|
||||
|
||||
const requests = [5, 2, 8, 3, 7];
|
||||
const serversLoads = loadBalance(requests, 3);
|
||||
expect(serversLoads).toEqual([12, 8, 5]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1 +1,54 @@
|
|||
export {};
|
||||
/**
|
||||
* Convert any string to CamelCase format
|
||||
*/
|
||||
export function toCamelCase(str: string): string {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert any string to SnakeCase format
|
||||
*/
|
||||
export function toSnakeCase(str: string): string {
|
||||
return str
|
||||
.replace(/([a-z])([A-Z])/g, '$1_$2') // Add underline between lowercase and uppercase letters
|
||||
.toLowerCase() // Convert to lowercase
|
||||
.replace(/[^a-z0-9]+/g, '_'); // Replace non-alphanumeric characters with underscores
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert any string to PascalCase format (first letter capitalized)
|
||||
*/
|
||||
export function toPascalCase(str: string): string {
|
||||
return str
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2') // Add space between lowercase and uppercase letters
|
||||
.replace(/[^a-zA-Z0-9]+/g, ' ') // Replace non-alphanumeric characters with spaces
|
||||
.split(' ') // Separate strings by spaces
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) // The first letter is capitalized, the rest are lowercase
|
||||
.join(''); // Combine into a string
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert CamelCase or SnakeCase string to string format with specified separator
|
||||
*/
|
||||
export function toSeparatedCase(str: string, separator: string = '_'): string {
|
||||
return str
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1' + separator + '$2')
|
||||
.replace(/[_\s]+/g, separator)
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the string to all uppercase and delimit it using the specified delimiter
|
||||
*/
|
||||
export function toUpperSeparatedCase(
|
||||
str: string,
|
||||
separator: string = '_',
|
||||
): string {
|
||||
return str
|
||||
.toUpperCase() // Convert all letters to uppercase
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1' + separator + '$2') // Add separator between lowercase letters and uppercase letters
|
||||
.replace(/[^A-Z0-9]+/g, separator) // Replace non-alphanumeric characters with separators
|
||||
.replace(new RegExp(`^${separator}|${separator}$`, 'g'), ''); // Remove the starting and ending separators
|
||||
}
|
||||
|
|
207
testToExample.ts
Normal file
207
testToExample.ts
Normal file
|
@ -0,0 +1,207 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
import { toPascalCase } from './test/utils';
|
||||
|
||||
const isReplaceMD = false;
|
||||
const START_MARKER = '[//]: # (No deletion!!! Start of Example Replace Section)';
|
||||
const END_MARKER = '[//]: # (No deletion!!! End of Example Replace Section)';
|
||||
|
||||
/**
|
||||
* Recursively retrieve all `.ts` files in a directory.
|
||||
*/
|
||||
function getAllTestFiles(dir: string): string[] {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
const files = entries
|
||||
.filter(file => !file.isDirectory() && file.name.endsWith('.ts'))
|
||||
.map(file => path.join(dir, file.name));
|
||||
|
||||
const directories = entries.filter(entry => entry.isDirectory());
|
||||
|
||||
for (const directory of directories) {
|
||||
files.push(...getAllTestFiles(path.join(dir, directory.name)));
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract test cases with `@example` from TypeScript files using AST.
|
||||
*/
|
||||
function extractExamplesFromFile(filePath: string): { name: string; body: string }[] {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||
const sourceFile = ts.createSourceFile(filePath, fileContent, ts.ScriptTarget.Latest, true);
|
||||
|
||||
const examples: { name: string; body: string }[] = [];
|
||||
|
||||
function visit(node: ts.Node) {
|
||||
if (
|
||||
ts.isCallExpression(node) && // Ensure it's a function call
|
||||
node.arguments.length >= 2 && // At least two arguments
|
||||
ts.isStringLiteral(node.arguments[0]) && // First argument is a string
|
||||
node.arguments[0].text.startsWith('@example') && // Matches @example
|
||||
ts.isArrowFunction(node.arguments[1]) // Second argument is an arrow function
|
||||
) {
|
||||
const exampleName = node.arguments[0].text.replace('@example ', '').trim();
|
||||
const bodyNode = node.arguments[1].body;
|
||||
|
||||
let exampleBody: string;
|
||||
if (ts.isBlock(bodyNode)) {
|
||||
// If it's a block, remove outer {}
|
||||
exampleBody = bodyNode.statements
|
||||
.map(stmt => stmt.getFullText(sourceFile))
|
||||
.join('')
|
||||
.trim();
|
||||
} else {
|
||||
// If it's a single expression, use it directly
|
||||
exampleBody = bodyNode.getFullText(sourceFile).trim();
|
||||
}
|
||||
|
||||
const transformedBody = exampleBody
|
||||
.replace(
|
||||
/expect\((.*?)\)\.(toEqual|toBe|toStrictEqual|toHaveLength|toMatchObject)\((.*?)\);/g,
|
||||
(match, actual, method, expected) => {
|
||||
return `console.log(${actual}); // ${expected.trim()}`;
|
||||
}
|
||||
)
|
||||
.trim();
|
||||
|
||||
examples.push({ name: exampleName, body: transformedBody });
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
|
||||
return examples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add examples to the corresponding class in the source file.
|
||||
*/
|
||||
function addExamplesToSourceFile(
|
||||
sourceFilePath: string,
|
||||
className: string,
|
||||
examples: { name: string; body: string }[]
|
||||
): void {
|
||||
if (!fs.existsSync(sourceFilePath)) {
|
||||
console.warn(`Source file not found: ${sourceFilePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceContent = fs.readFileSync(sourceFilePath, 'utf-8');
|
||||
const sourceFile = ts.createSourceFile(sourceFilePath, sourceContent, ts.ScriptTarget.Latest, true);
|
||||
|
||||
let updatedContent = sourceContent;
|
||||
|
||||
const classNode = sourceFile.statements.find(
|
||||
stmt => ts.isClassDeclaration(stmt) && stmt.name?.text === className
|
||||
) as ts.ClassDeclaration | undefined;
|
||||
|
||||
if (classNode) {
|
||||
const classStart = classNode.getStart(sourceFile);
|
||||
const classEnd = classNode.getEnd();
|
||||
const classText = classNode.getFullText(sourceFile);
|
||||
|
||||
// 提取注释内容
|
||||
const existingCommentMatch = classText.match(/\/\*\*([\s\S]*?)\*\//);
|
||||
if (!existingCommentMatch) {
|
||||
console.warn(`No existing comment found for class: ${className}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const existingCommentInner = existingCommentMatch[1]; // 提取注释内容(不包括`/**`和`*/`)
|
||||
|
||||
// 替换 @example 部分
|
||||
const exampleSection = examples
|
||||
.map(
|
||||
example =>
|
||||
`@example \n * \/\/ ${example.name} \n${example.body
|
||||
.split('\n')
|
||||
.map(line => ` * ${line}`)
|
||||
.join('\n')}\n * \n`
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
let newComment = '';
|
||||
if (existingCommentInner.includes('@example')) {
|
||||
newComment = existingCommentInner.replace(/@example[\s\S]*?(?=\*\/|$)/g, exampleSection);
|
||||
} else {
|
||||
newComment = existingCommentInner + `\n * ${exampleSection}`;
|
||||
}
|
||||
|
||||
|
||||
// 替换原始内容
|
||||
updatedContent =
|
||||
sourceContent.slice(0, classStart - existingCommentInner.length - 1) +
|
||||
newComment +
|
||||
classText.slice(existingCommentMatch[0].length).trim() +
|
||||
sourceContent.slice(classEnd);
|
||||
}
|
||||
|
||||
fs.writeFileSync(sourceFilePath, updatedContent, 'utf-8');
|
||||
console.log(`Updated examples in ${sourceFilePath}`);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process all test files and update README.md and source files.
|
||||
*/
|
||||
function updateExamples(testDir: string, readmePath: string, sourceBaseDir: string): void {
|
||||
const testFiles = getAllTestFiles(testDir);
|
||||
|
||||
let allExamples: string[] = [];
|
||||
for (const file of testFiles) {
|
||||
const examples = extractExamplesFromFile(file);
|
||||
|
||||
if (examples.length === 0) {
|
||||
console.log(`No @example found in test file: ${file}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const relativePath = path.relative(testDir, file);
|
||||
const sourceFilePath = path.resolve(sourceBaseDir, relativePath.replace('.test.ts', '.ts'));
|
||||
const className = path.basename(sourceFilePath, '.ts');
|
||||
|
||||
addExamplesToSourceFile(sourceFilePath, toPascalCase(className), examples);
|
||||
|
||||
allExamples = allExamples.concat(
|
||||
examples.map(example => `### ${example.name}\n\`\`\`typescript\n${example.body}\n\`\`\``)
|
||||
);
|
||||
}
|
||||
|
||||
if (isReplaceMD && allExamples.length > 0) {
|
||||
replaceExamplesInReadme(readmePath, allExamples);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace content between markers in README.md.
|
||||
*/
|
||||
function replaceExamplesInReadme(readmePath: string, newExamples: string[]): void {
|
||||
const readmeContent = fs.readFileSync(readmePath, 'utf-8');
|
||||
|
||||
const startIdx = readmeContent.indexOf(START_MARKER);
|
||||
const endIdx = readmeContent.indexOf(END_MARKER);
|
||||
|
||||
if (startIdx === -1 || endIdx === -1) {
|
||||
throw new Error(`Markers not found in ${readmePath}`);
|
||||
}
|
||||
|
||||
const before = readmeContent.slice(0, startIdx + START_MARKER.length);
|
||||
const after = readmeContent.slice(endIdx);
|
||||
|
||||
const updatedContent = `${before}\n\n${newExamples.join('\n\n')}\n\n${after}`;
|
||||
fs.writeFileSync(readmePath, updatedContent, 'utf-8');
|
||||
|
||||
console.log(`README.md updated with new examples.`);
|
||||
}
|
||||
|
||||
// Run the script
|
||||
const testDir = path.resolve(__dirname, 'test/unit');
|
||||
const readmePath = path.resolve(__dirname, 'README.md');
|
||||
const sourceBaseDir = path.resolve(__dirname, 'src');
|
||||
|
||||
updateExamples(testDir, readmePath, sourceBaseDir);
|
Loading…
Reference in a new issue