@amirdaraee/namewise 0.4.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +102 -0
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +14 -7
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/rename.js +1 -1
- package/dist/cli/rename.js.map +1 -1
- package/dist/parsers/factory.d.ts +2 -1
- package/dist/parsers/factory.d.ts.map +1 -1
- package/dist/parsers/factory.js +9 -6
- package/dist/parsers/factory.js.map +1 -1
- package/dist/parsers/pdf-parser.d.ts +1 -0
- package/dist/parsers/pdf-parser.d.ts.map +1 -1
- package/dist/parsers/pdf-parser.js +20 -1
- package/dist/parsers/pdf-parser.js.map +1 -1
- package/dist/services/claude-service.d.ts.map +1 -1
- package/dist/services/claude-service.js +57 -17
- package/dist/services/claude-service.js.map +1 -1
- package/dist/services/lmstudio-service.d.ts.map +1 -1
- package/dist/services/lmstudio-service.js +7 -0
- package/dist/services/lmstudio-service.js.map +1 -1
- package/dist/services/ollama-service.d.ts +1 -0
- package/dist/services/ollama-service.d.ts.map +1 -1
- package/dist/services/ollama-service.js +54 -15
- package/dist/services/ollama-service.js.map +1 -1
- package/dist/services/openai-service.d.ts.map +1 -1
- package/dist/services/openai-service.js +57 -18
- package/dist/services/openai-service.js.map +1 -1
- package/dist/utils/pdf-to-image.d.ts +11 -0
- package/dist/utils/pdf-to-image.d.ts.map +1 -0
- package/dist/utils/pdf-to-image.js +104 -0
- package/dist/utils/pdf-to-image.js.map +1 -0
- package/eng.traineddata +0 -0
- package/package.json +5 -2
- package/src/cli/commands.ts +14 -7
- package/src/cli/rename.ts +1 -1
- package/src/parsers/factory.ts +11 -7
- package/src/parsers/pdf-parser.ts +22 -1
- package/src/services/claude-service.ts +61 -18
- package/src/services/lmstudio-service.ts +9 -0
- package/src/services/ollama-service.ts +68 -15
- package/src/services/openai-service.ts +61 -19
- package/src/utils/pdf-to-image.ts +137 -0
- package/tests/integration/end-to-end.test.ts +9 -9
- package/tests/unit/cli/commands.test.ts +9 -3
- package/tests/unit/utils/pdf-to-image.test.ts +127 -0
|
@@ -18,26 +18,68 @@ export class OpenAIService implements AIProvider {
|
|
|
18
18
|
const convention = namingConvention as NamingConvention;
|
|
19
19
|
const fileCategory = category as FileCategory;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
namingConvention: convention,
|
|
25
|
-
category: fileCategory,
|
|
26
|
-
fileInfo
|
|
27
|
-
});
|
|
28
|
-
|
|
21
|
+
// Check if this is a scanned PDF image
|
|
22
|
+
const isScannedPDF = content.startsWith('[SCANNED_PDF_IMAGE]:');
|
|
23
|
+
|
|
29
24
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
let response;
|
|
26
|
+
|
|
27
|
+
if (isScannedPDF) {
|
|
28
|
+
// Extract base64 image data
|
|
29
|
+
const imageBase64 = content.replace('[SCANNED_PDF_IMAGE]:', '');
|
|
30
|
+
|
|
31
|
+
const prompt = buildFileNamePrompt({
|
|
32
|
+
content: 'This is a scanned PDF document converted to an image. Please analyze the image and extract the main content to generate an appropriate filename.',
|
|
33
|
+
originalName,
|
|
34
|
+
namingConvention: convention,
|
|
35
|
+
category: fileCategory,
|
|
36
|
+
fileInfo
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
response = await this.client.chat.completions.create({
|
|
40
|
+
model: 'gpt-4o', // Use GPT-4 with vision capabilities
|
|
41
|
+
messages: [
|
|
42
|
+
{
|
|
43
|
+
role: 'user',
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: 'text',
|
|
47
|
+
text: prompt
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'image_url',
|
|
51
|
+
image_url: {
|
|
52
|
+
url: imageBase64
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
max_tokens: 100,
|
|
59
|
+
temperature: 0.3
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
// Standard text processing
|
|
63
|
+
const prompt = buildFileNamePrompt({
|
|
64
|
+
content,
|
|
65
|
+
originalName,
|
|
66
|
+
namingConvention: convention,
|
|
67
|
+
category: fileCategory,
|
|
68
|
+
fileInfo
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
response = await this.client.chat.completions.create({
|
|
72
|
+
model: 'gpt-3.5-turbo',
|
|
73
|
+
messages: [
|
|
74
|
+
{
|
|
75
|
+
role: 'user',
|
|
76
|
+
content: prompt
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
max_tokens: 100,
|
|
80
|
+
temperature: 0.3
|
|
81
|
+
});
|
|
82
|
+
}
|
|
41
83
|
|
|
42
84
|
const suggestedName = response.choices[0]?.message?.content?.trim() || 'untitled-document';
|
|
43
85
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { pdfToPng } from 'pdf-to-png-converter';
|
|
2
|
+
import { createCanvas, loadImage, DOMMatrix } from 'canvas';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
|
|
5
|
+
// Polyfill DOMMatrix for Node.js environments (required by pdf-to-png-converter)
|
|
6
|
+
if (typeof global !== 'undefined' && !global.DOMMatrix) {
|
|
7
|
+
global.DOMMatrix = DOMMatrix as any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Polyfill process.getBuiltinModule for Node.js < 22.3.0
|
|
11
|
+
if (typeof process !== 'undefined' && !process.getBuiltinModule) {
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
(process as any).getBuiltinModule = (id: string) => {
|
|
14
|
+
try {
|
|
15
|
+
return require(id);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PDFToImageOptions {
|
|
23
|
+
scale?: number;
|
|
24
|
+
format?: 'png' | 'jpeg';
|
|
25
|
+
firstPageOnly?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class PDFToImageConverter {
|
|
29
|
+
// Claude's maximum image size is 5MB
|
|
30
|
+
private static readonly MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024;
|
|
31
|
+
|
|
32
|
+
static async convertFirstPageToBase64(
|
|
33
|
+
pdfBuffer: Buffer,
|
|
34
|
+
options: PDFToImageOptions = {}
|
|
35
|
+
): Promise<string> {
|
|
36
|
+
const {
|
|
37
|
+
scale = 2.0, // Higher scale for better quality (1-3 recommended)
|
|
38
|
+
format = 'png'
|
|
39
|
+
} = options;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Convert PDF to PNG using pdf-to-png-converter
|
|
43
|
+
// This package handles all the canvas/image compatibility issues
|
|
44
|
+
const pngPages = await pdfToPng(pdfBuffer as any, {
|
|
45
|
+
disableFontFace: false,
|
|
46
|
+
useSystemFonts: false,
|
|
47
|
+
pagesToProcess: [1], // Only convert first page
|
|
48
|
+
verbosityLevel: 0,
|
|
49
|
+
viewportScale: scale
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!pngPages || pngPages.length === 0) {
|
|
53
|
+
throw new Error('No pages could be converted from PDF');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Get the first page
|
|
57
|
+
const firstPage = pngPages[0];
|
|
58
|
+
|
|
59
|
+
if (!firstPage || !firstPage.content) {
|
|
60
|
+
throw new Error('First page conversion failed');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Load the PNG image for optimization
|
|
64
|
+
const img = await loadImage(firstPage.content);
|
|
65
|
+
|
|
66
|
+
// Always use JPEG for better compression and size control
|
|
67
|
+
// Try different quality levels to fit under the size limit
|
|
68
|
+
const qualities = [0.85, 0.7, 0.6, 0.5, 0.4, 0.3];
|
|
69
|
+
|
|
70
|
+
for (const quality of qualities) {
|
|
71
|
+
const canvas = createCanvas(img.width, img.height);
|
|
72
|
+
const ctx = canvas.getContext('2d');
|
|
73
|
+
ctx.drawImage(img, 0, 0);
|
|
74
|
+
|
|
75
|
+
const dataUrl = canvas.toDataURL('image/jpeg', quality);
|
|
76
|
+
const sizeInBytes = Math.ceil((dataUrl.length - 'data:image/jpeg;base64,'.length) * 0.75);
|
|
77
|
+
|
|
78
|
+
if (sizeInBytes <= this.MAX_IMAGE_SIZE_BYTES) {
|
|
79
|
+
return dataUrl;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// If still too large, reduce dimensions
|
|
84
|
+
const scaleFactor = 0.7;
|
|
85
|
+
const newWidth = Math.floor(img.width * scaleFactor);
|
|
86
|
+
const newHeight = Math.floor(img.height * scaleFactor);
|
|
87
|
+
|
|
88
|
+
const canvas = createCanvas(newWidth, newHeight);
|
|
89
|
+
const ctx = canvas.getContext('2d');
|
|
90
|
+
ctx.drawImage(img, 0, 0, newWidth, newHeight);
|
|
91
|
+
|
|
92
|
+
// Try with reduced dimensions
|
|
93
|
+
for (const quality of qualities) {
|
|
94
|
+
const dataUrl = canvas.toDataURL('image/jpeg', quality);
|
|
95
|
+
const sizeInBytes = Math.ceil((dataUrl.length - 'data:image/jpeg;base64,'.length) * 0.75);
|
|
96
|
+
|
|
97
|
+
if (sizeInBytes <= this.MAX_IMAGE_SIZE_BYTES) {
|
|
98
|
+
return dataUrl;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Last resort: heavily compressed small image
|
|
103
|
+
const smallCanvas = createCanvas(Math.floor(newWidth * 0.5), Math.floor(newHeight * 0.5));
|
|
104
|
+
const smallCtx = smallCanvas.getContext('2d');
|
|
105
|
+
smallCtx.drawImage(img, 0, 0, smallCanvas.width, smallCanvas.height);
|
|
106
|
+
|
|
107
|
+
return smallCanvas.toDataURL('image/jpeg', 0.3);
|
|
108
|
+
|
|
109
|
+
} catch (error) {
|
|
110
|
+
// Enhanced error logging for debugging
|
|
111
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
112
|
+
const errorStack = error instanceof Error ? error.stack : '';
|
|
113
|
+
|
|
114
|
+
console.error('PDF to image conversion detailed error:', {
|
|
115
|
+
message: errorMessage,
|
|
116
|
+
stack: errorStack,
|
|
117
|
+
errorType: error?.constructor?.name
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
throw new Error(`PDF to image conversion failed: ${errorMessage}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static isScannedPDF(extractedText: string): boolean {
|
|
125
|
+
// Heuristics to detect scanned/image-only PDFs
|
|
126
|
+
const textLength = extractedText.trim().length;
|
|
127
|
+
const wordCount = extractedText.trim().split(/\s+/).filter(w => w.length > 0).length;
|
|
128
|
+
|
|
129
|
+
// Consider it scanned if:
|
|
130
|
+
// - Very little text (< 50 characters)
|
|
131
|
+
// - Very few words (< 10 words)
|
|
132
|
+
// - High ratio of non-alphabetic characters
|
|
133
|
+
const nonAlphaRatio = (extractedText.length - extractedText.replace(/[^a-zA-Z]/g, '').length) / Math.max(extractedText.length, 1);
|
|
134
|
+
|
|
135
|
+
return textLength < 50 || wordCount < 10 || nonAlphaRatio > 0.9;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -34,9 +34,9 @@ describe('End-to-End Integration Tests', () => {
|
|
|
34
34
|
describe('CLI Integration', () => {
|
|
35
35
|
it('should show help message', async () => {
|
|
36
36
|
const { stdout } = await execAsync(`node ${cliPath} --help`);
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
expect(stdout).toContain('AI-powered CLI tool that intelligently renames files based on their content');
|
|
39
|
-
expect(stdout).toContain('rename [options]
|
|
39
|
+
expect(stdout).toContain('rename [options] [directory]');
|
|
40
40
|
expect(stdout).toContain('Commands:');
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -95,13 +95,13 @@ describe('End-to-End Integration Tests', () => {
|
|
|
95
95
|
expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
it('should
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
98
|
+
it('should accept optional directory argument', async () => {
|
|
99
|
+
const { stdout } = await execAsync(`node ${cliPath} rename --help`);
|
|
100
|
+
|
|
101
|
+
// Verify directory is shown as optional (in brackets) in help
|
|
102
|
+
expect(stdout).toContain('[directory]');
|
|
103
|
+
expect(stdout).toContain('current directory');
|
|
104
|
+
expect(stdout).toContain('(default: ".")');
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
it('should handle non-existent directory', async () => {
|
|
@@ -113,7 +113,9 @@ describe('CLI Commands', () => {
|
|
|
113
113
|
case: 'kebab-case',
|
|
114
114
|
template: 'general',
|
|
115
115
|
name: undefined,
|
|
116
|
-
date: 'none'
|
|
116
|
+
date: 'none',
|
|
117
|
+
baseUrl: undefined,
|
|
118
|
+
model: undefined
|
|
117
119
|
});
|
|
118
120
|
});
|
|
119
121
|
|
|
@@ -133,7 +135,9 @@ describe('CLI Commands', () => {
|
|
|
133
135
|
case: 'kebab-case',
|
|
134
136
|
template: 'general',
|
|
135
137
|
name: undefined,
|
|
136
|
-
date: 'none'
|
|
138
|
+
date: 'none',
|
|
139
|
+
baseUrl: undefined,
|
|
140
|
+
model: undefined
|
|
137
141
|
});
|
|
138
142
|
});
|
|
139
143
|
|
|
@@ -156,7 +160,9 @@ describe('CLI Commands', () => {
|
|
|
156
160
|
case: 'kebab-case',
|
|
157
161
|
template: 'general',
|
|
158
162
|
name: undefined,
|
|
159
|
-
date: 'none'
|
|
163
|
+
date: 'none',
|
|
164
|
+
baseUrl: undefined,
|
|
165
|
+
model: undefined
|
|
160
166
|
});
|
|
161
167
|
});
|
|
162
168
|
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
|
+
import { PDFToImageConverter } from '../../../src/utils/pdf-to-image.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
describe('PDFToImageConverter', () => {
|
|
7
|
+
let samplePdfBuffer: Buffer;
|
|
8
|
+
const testDataDir = path.join(process.cwd(), 'tests/data');
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
// Load sample PDF for testing
|
|
12
|
+
const pdfPath = path.join(testDataDir, 'sample-pdf.pdf');
|
|
13
|
+
samplePdfBuffer = fs.readFileSync(pdfPath);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('Integration with PDF Parser', () => {
|
|
17
|
+
it('should successfully convert scanned PDF through parser workflow', async () => {
|
|
18
|
+
// This simulates what happens in the actual PDF parser
|
|
19
|
+
const { PDFParser } = await import('../../../src/parsers/pdf-parser.js');
|
|
20
|
+
const parser = new PDFParser();
|
|
21
|
+
|
|
22
|
+
// Create a minimal scanned PDF scenario
|
|
23
|
+
const pdfPath = path.join(testDataDir, 'sample-pdf.pdf');
|
|
24
|
+
|
|
25
|
+
// Parse the PDF (this will trigger conversion if detected as scanned)
|
|
26
|
+
const result = await parser.parse(pdfPath);
|
|
27
|
+
|
|
28
|
+
// The parser should complete without throwing errors
|
|
29
|
+
expect(result).toBeDefined();
|
|
30
|
+
expect(result.content).toBeDefined();
|
|
31
|
+
}, 15000);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('convertFirstPageToBase64()', () => {
|
|
35
|
+
it('should convert PDF first page to base64 JPEG image', async () => {
|
|
36
|
+
const result = await PDFToImageConverter.convertFirstPageToBase64(samplePdfBuffer);
|
|
37
|
+
|
|
38
|
+
// Verify it's a base64 data URL (always JPEG for size optimization)
|
|
39
|
+
expect(result).toMatch(/^data:image\/jpeg;base64,/);
|
|
40
|
+
|
|
41
|
+
// Verify it has actual content
|
|
42
|
+
expect(result.length).toBeGreaterThan(100);
|
|
43
|
+
|
|
44
|
+
// Verify base64 encoding is valid
|
|
45
|
+
const base64Data = result.split(',')[1];
|
|
46
|
+
expect(() => Buffer.from(base64Data, 'base64')).not.toThrow();
|
|
47
|
+
}, 10000); // 10 second timeout for PDF processing
|
|
48
|
+
|
|
49
|
+
it('should respect format option when specified', async () => {
|
|
50
|
+
const result = await PDFToImageConverter.convertFirstPageToBase64(samplePdfBuffer, {
|
|
51
|
+
format: 'jpeg'
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Verify it's a base64 data URL (always JPEG for size optimization)
|
|
55
|
+
expect(result).toMatch(/^data:image\/jpeg;base64,/);
|
|
56
|
+
|
|
57
|
+
// Verify it has actual content
|
|
58
|
+
expect(result.length).toBeGreaterThan(100);
|
|
59
|
+
}, 10000);
|
|
60
|
+
|
|
61
|
+
it('should use custom scale factor', async () => {
|
|
62
|
+
const resultScale1 = await PDFToImageConverter.convertFirstPageToBase64(samplePdfBuffer, {
|
|
63
|
+
scale: 1.0
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const resultScale2 = await PDFToImageConverter.convertFirstPageToBase64(samplePdfBuffer, {
|
|
67
|
+
scale: 2.0
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Both should be JPEG format
|
|
71
|
+
expect(resultScale1).toMatch(/^data:image\/jpeg;base64,/);
|
|
72
|
+
expect(resultScale2).toMatch(/^data:image\/jpeg;base64,/);
|
|
73
|
+
|
|
74
|
+
// Higher scale should generally produce larger image (though compression may vary)
|
|
75
|
+
expect(resultScale2.length).toBeGreaterThan(0);
|
|
76
|
+
expect(resultScale1.length).toBeGreaterThan(0);
|
|
77
|
+
}, 15000);
|
|
78
|
+
|
|
79
|
+
it('should handle invalid PDF buffer', async () => {
|
|
80
|
+
const invalidBuffer = Buffer.from('This is not a PDF');
|
|
81
|
+
|
|
82
|
+
await expect(
|
|
83
|
+
PDFToImageConverter.convertFirstPageToBase64(invalidBuffer)
|
|
84
|
+
).rejects.toThrow(/PDF to image conversion failed/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should handle empty buffer', async () => {
|
|
88
|
+
const emptyBuffer = Buffer.from([]);
|
|
89
|
+
|
|
90
|
+
await expect(
|
|
91
|
+
PDFToImageConverter.convertFirstPageToBase64(emptyBuffer)
|
|
92
|
+
).rejects.toThrow(/PDF to image conversion failed/);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('isScannedPDF()', () => {
|
|
97
|
+
it('should detect scanned PDF with very little text', () => {
|
|
98
|
+
const scannedText = 'abc';
|
|
99
|
+
expect(PDFToImageConverter.isScannedPDF(scannedText)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should detect scanned PDF with few words', () => {
|
|
103
|
+
const scannedText = 'one two three four';
|
|
104
|
+
expect(PDFToImageConverter.isScannedPDF(scannedText)).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should detect scanned PDF with high non-alphabetic ratio', () => {
|
|
108
|
+
const scannedText = '### %%% $$$ ### %%%';
|
|
109
|
+
expect(PDFToImageConverter.isScannedPDF(scannedText)).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should not detect normal PDF as scanned', () => {
|
|
113
|
+
const normalText = 'This is a normal document with plenty of readable text content that was generated from a text-based PDF file.';
|
|
114
|
+
expect(PDFToImageConverter.isScannedPDF(normalText)).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should detect empty text as scanned', () => {
|
|
118
|
+
const emptyText = '';
|
|
119
|
+
expect(PDFToImageConverter.isScannedPDF(emptyText)).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should detect whitespace-only text as scanned', () => {
|
|
123
|
+
const whitespaceText = ' \n \t ';
|
|
124
|
+
expect(PDFToImageConverter.isScannedPDF(whitespaceText)).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|