jww-parser 2025.12.2 → 2025.12.4
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/dist/index.d.ts +809 -30
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +845 -166
- package/package.json +3 -3
- package/wasm/jww-parser.wasm +0 -0
- package/dist/index.d.mts +0 -168
- package/dist/index.mjs +0 -130
package/dist/index.js
CHANGED
|
@@ -1,167 +1,846 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
1
|
+
/**
|
|
2
|
+
* JWW (Jw_cad) file parser and DXF converter
|
|
3
|
+
*
|
|
4
|
+
* This module provides functionality to parse JWW binary files
|
|
5
|
+
* and convert them to DXF format using WebAssembly.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Error Classes
|
|
11
|
+
// =============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* Error codes for JWW parser operations
|
|
14
|
+
*/
|
|
15
|
+
export var JwwErrorCode;
|
|
16
|
+
(function (JwwErrorCode) {
|
|
17
|
+
/** Parser has not been initialized */
|
|
18
|
+
JwwErrorCode["NOT_INITIALIZED"] = "NOT_INITIALIZED";
|
|
19
|
+
/** WASM module failed to load */
|
|
20
|
+
JwwErrorCode["WASM_LOAD_FAILED"] = "WASM_LOAD_FAILED";
|
|
21
|
+
/** WASM functions not available after timeout */
|
|
22
|
+
JwwErrorCode["WASM_TIMEOUT"] = "WASM_TIMEOUT";
|
|
23
|
+
/** Invalid JWW file signature */
|
|
24
|
+
JwwErrorCode["INVALID_SIGNATURE"] = "INVALID_SIGNATURE";
|
|
25
|
+
/** Unsupported JWW version */
|
|
26
|
+
JwwErrorCode["UNSUPPORTED_VERSION"] = "UNSUPPORTED_VERSION";
|
|
27
|
+
/** General parse error */
|
|
28
|
+
JwwErrorCode["PARSE_ERROR"] = "PARSE_ERROR";
|
|
29
|
+
/** DXF conversion error */
|
|
30
|
+
JwwErrorCode["CONVERSION_ERROR"] = "CONVERSION_ERROR";
|
|
31
|
+
/** Validation error */
|
|
32
|
+
JwwErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
|
33
|
+
/** Memory allocation error */
|
|
34
|
+
JwwErrorCode["MEMORY_ERROR"] = "MEMORY_ERROR";
|
|
35
|
+
/** Invalid argument provided */
|
|
36
|
+
JwwErrorCode["INVALID_ARGUMENT"] = "INVALID_ARGUMENT";
|
|
37
|
+
})(JwwErrorCode || (JwwErrorCode = {}));
|
|
38
|
+
/**
|
|
39
|
+
* Base error class for JWW parser errors
|
|
40
|
+
*/
|
|
41
|
+
export class JwwParserError extends Error {
|
|
42
|
+
constructor(code, message, options) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "JwwParserError";
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.cause = options?.cause;
|
|
47
|
+
this.context = options?.context;
|
|
48
|
+
this.timestamp = new Date();
|
|
49
|
+
// Maintain proper stack trace for where our error was thrown
|
|
50
|
+
if (Error.captureStackTrace) {
|
|
51
|
+
Error.captureStackTrace(this, JwwParserError);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Returns a detailed string representation of the error
|
|
56
|
+
*/
|
|
57
|
+
toDetailedString() {
|
|
58
|
+
let details = `[${this.code}] ${this.message}`;
|
|
59
|
+
if (this.context) {
|
|
60
|
+
details += `\nContext: ${JSON.stringify(this.context, null, 2)}`;
|
|
61
|
+
}
|
|
62
|
+
if (this.cause) {
|
|
63
|
+
details += `\nCaused by: ${this.cause.message}`;
|
|
64
|
+
}
|
|
65
|
+
return details;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Converts the error to a plain object for logging/serialization
|
|
69
|
+
*/
|
|
70
|
+
toJSON() {
|
|
71
|
+
return {
|
|
72
|
+
name: this.name,
|
|
73
|
+
code: this.code,
|
|
74
|
+
message: this.message,
|
|
75
|
+
context: this.context,
|
|
76
|
+
timestamp: this.timestamp.toISOString(),
|
|
77
|
+
cause: this.cause?.message,
|
|
78
|
+
stack: this.stack,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Error thrown when the parser is not initialized
|
|
84
|
+
*/
|
|
85
|
+
export class NotInitializedError extends JwwParserError {
|
|
86
|
+
constructor() {
|
|
87
|
+
super(JwwErrorCode.NOT_INITIALIZED, "Parser not initialized. Call init() first or use createParser() factory function.");
|
|
88
|
+
this.name = "NotInitializedError";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Error thrown when WASM module fails to load
|
|
93
|
+
*/
|
|
94
|
+
export class WasmLoadError extends JwwParserError {
|
|
95
|
+
constructor(message, cause) {
|
|
96
|
+
super(JwwErrorCode.WASM_LOAD_FAILED, message, { cause });
|
|
97
|
+
this.name = "WasmLoadError";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Error thrown when file validation fails
|
|
102
|
+
*/
|
|
103
|
+
export class ValidationError extends JwwParserError {
|
|
104
|
+
constructor(message, issues) {
|
|
105
|
+
super(JwwErrorCode.VALIDATION_ERROR, message, {
|
|
106
|
+
context: { issues },
|
|
107
|
+
});
|
|
108
|
+
this.name = "ValidationError";
|
|
109
|
+
this.issues = issues;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Error thrown during parsing
|
|
114
|
+
*/
|
|
115
|
+
export class ParseError extends JwwParserError {
|
|
116
|
+
constructor(message, options) {
|
|
117
|
+
super(JwwErrorCode.PARSE_ERROR, message, {
|
|
118
|
+
cause: options?.cause,
|
|
119
|
+
context: {
|
|
120
|
+
offset: options?.offset,
|
|
121
|
+
section: options?.section,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
this.name = "ParseError";
|
|
125
|
+
this.offset = options?.offset;
|
|
126
|
+
this.section = options?.section;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// =============================================================================
|
|
130
|
+
// Helper Functions
|
|
131
|
+
// =============================================================================
|
|
132
|
+
/**
|
|
133
|
+
* Format bytes to human-readable string
|
|
134
|
+
*/
|
|
135
|
+
function formatBytes(bytes) {
|
|
136
|
+
if (bytes < 1024)
|
|
137
|
+
return `${bytes} B`;
|
|
138
|
+
if (bytes < 1024 * 1024)
|
|
139
|
+
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
140
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
141
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
142
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get file size category based on byte count
|
|
146
|
+
*/
|
|
147
|
+
function getSizeCategory(bytes) {
|
|
148
|
+
if (bytes < 100 * 1024)
|
|
149
|
+
return "small"; // < 100KB
|
|
150
|
+
if (bytes < 1024 * 1024)
|
|
151
|
+
return "medium"; // < 1MB
|
|
152
|
+
if (bytes < 10 * 1024 * 1024)
|
|
153
|
+
return "large"; // < 10MB
|
|
154
|
+
return "very_large"; // >= 10MB
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Create a parse error from WASM result
|
|
158
|
+
*/
|
|
159
|
+
function createParseError(result) {
|
|
160
|
+
const message = result.error || "Parse failed";
|
|
161
|
+
// Try to extract more specific error type
|
|
162
|
+
if (message.includes("invalid JWW signature")) {
|
|
163
|
+
return new ParseError("Invalid JWW file format: missing or corrupt signature", {
|
|
164
|
+
section: "header",
|
|
165
|
+
offset: 0,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (message.includes("unsupported") && message.includes("version")) {
|
|
169
|
+
return new ParseError(message, {
|
|
170
|
+
section: "header",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return new ParseError(message, {
|
|
174
|
+
offset: result.offset,
|
|
175
|
+
section: result.section,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// =============================================================================
|
|
179
|
+
// JwwParser Class
|
|
180
|
+
// =============================================================================
|
|
181
|
+
/**
|
|
182
|
+
* JWW file parser with WebAssembly backend
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* // Using the factory function (recommended)
|
|
187
|
+
* const parser = await createParser();
|
|
188
|
+
* const doc = parser.parse(fileData);
|
|
189
|
+
*
|
|
190
|
+
* // Manual initialization
|
|
191
|
+
* const parser = new JwwParser();
|
|
192
|
+
* await parser.init();
|
|
193
|
+
* const doc = parser.parse(fileData);
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
export class JwwParser {
|
|
197
|
+
/**
|
|
198
|
+
* Create a new JWW parser instance
|
|
199
|
+
* @param wasmPath - Path to the jww-parser.wasm file
|
|
200
|
+
*/
|
|
201
|
+
constructor(wasmPath) {
|
|
202
|
+
this.initialized = false;
|
|
203
|
+
this.initPromise = null;
|
|
204
|
+
this.goInstance = null;
|
|
205
|
+
this.wasmInstance = null;
|
|
206
|
+
// Debug and logging
|
|
207
|
+
this.debugOptions = { enabled: false };
|
|
208
|
+
this.debugLogs = [];
|
|
209
|
+
// Statistics
|
|
210
|
+
this.stats = {
|
|
211
|
+
parseCount: 0,
|
|
212
|
+
totalBytesProcessed: 0,
|
|
213
|
+
parseTimes: [],
|
|
214
|
+
errorCount: 0,
|
|
215
|
+
};
|
|
216
|
+
this.wasmPath = wasmPath || this.getDefaultWasmPath();
|
|
217
|
+
}
|
|
218
|
+
getDefaultWasmPath() {
|
|
219
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
220
|
+
return new URL("../wasm/jww-parser.wasm", import.meta.url).pathname;
|
|
221
|
+
}
|
|
222
|
+
return "jww-parser.wasm";
|
|
223
|
+
}
|
|
224
|
+
// ===========================================================================
|
|
225
|
+
// Debug Methods
|
|
226
|
+
// ===========================================================================
|
|
227
|
+
/**
|
|
228
|
+
* Enable or configure debug mode
|
|
229
|
+
* @param options - Debug configuration options
|
|
230
|
+
*/
|
|
231
|
+
setDebug(options) {
|
|
232
|
+
if (typeof options === "boolean") {
|
|
233
|
+
this.debugOptions = { enabled: options };
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
this.debugOptions = { ...this.debugOptions, ...options };
|
|
237
|
+
}
|
|
238
|
+
// Also set debug mode in WASM if available
|
|
239
|
+
if (this.initialized && typeof globalThis.jwwSetDebug === "function") {
|
|
240
|
+
globalThis.jwwSetDebug(this.debugOptions.enabled ?? false);
|
|
241
|
+
}
|
|
242
|
+
this.log("info", "Debug mode " + (this.debugOptions.enabled ? "enabled" : "disabled"));
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Get debug logs
|
|
246
|
+
* @param level - Optional minimum log level to filter
|
|
247
|
+
* @returns Array of debug log entries
|
|
248
|
+
*/
|
|
249
|
+
getDebugLogs(level) {
|
|
250
|
+
if (!level)
|
|
251
|
+
return [...this.debugLogs];
|
|
252
|
+
const levels = ["debug", "info", "warn", "error"];
|
|
253
|
+
const minIndex = levels.indexOf(level);
|
|
254
|
+
return this.debugLogs.filter((entry) => levels.indexOf(entry.level) >= minIndex);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Clear debug logs
|
|
258
|
+
*/
|
|
259
|
+
clearDebugLogs() {
|
|
260
|
+
this.debugLogs = [];
|
|
261
|
+
}
|
|
262
|
+
log(level, message, data) {
|
|
263
|
+
if (!this.debugOptions.enabled)
|
|
264
|
+
return;
|
|
265
|
+
const levels = ["debug", "info", "warn", "error"];
|
|
266
|
+
const minLevel = this.debugOptions.logLevel || "info";
|
|
267
|
+
if (levels.indexOf(level) < levels.indexOf(minLevel))
|
|
268
|
+
return;
|
|
269
|
+
const entry = {
|
|
270
|
+
timestamp: new Date(),
|
|
271
|
+
level,
|
|
272
|
+
message,
|
|
273
|
+
data: this.debugOptions.includeMemoryUsage
|
|
274
|
+
? { ...data, memory: this.getMemoryStats() }
|
|
275
|
+
: data,
|
|
276
|
+
};
|
|
277
|
+
// Store log entry
|
|
278
|
+
this.debugLogs.push(entry);
|
|
279
|
+
if (this.debugLogs.length > (this.debugOptions.maxLogEntries || 1000)) {
|
|
280
|
+
this.debugLogs.shift();
|
|
281
|
+
}
|
|
282
|
+
// Call debug callback if provided
|
|
283
|
+
if (this.debugOptions.onDebug) {
|
|
284
|
+
this.debugOptions.onDebug(entry);
|
|
285
|
+
}
|
|
286
|
+
// Log to console if enabled
|
|
287
|
+
if (this.debugOptions.logToConsole) {
|
|
288
|
+
const consoleFn = level === "error"
|
|
289
|
+
? console.error
|
|
290
|
+
: level === "warn"
|
|
291
|
+
? console.warn
|
|
292
|
+
: level === "debug"
|
|
293
|
+
? console.debug
|
|
294
|
+
: console.log;
|
|
295
|
+
consoleFn(`[JwwParser:${level}] ${message}`, data || "");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// ===========================================================================
|
|
299
|
+
// Initialization Methods
|
|
300
|
+
// ===========================================================================
|
|
301
|
+
/**
|
|
302
|
+
* Check if the parser is initialized
|
|
303
|
+
*/
|
|
304
|
+
isInitialized() {
|
|
305
|
+
return this.initialized;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Initialize the WASM module
|
|
309
|
+
* Must be called before using parse methods
|
|
310
|
+
*/
|
|
311
|
+
async init() {
|
|
312
|
+
if (this.initialized)
|
|
313
|
+
return;
|
|
314
|
+
if (this.initPromise)
|
|
315
|
+
return this.initPromise;
|
|
316
|
+
this.log("info", "Initializing WASM module");
|
|
317
|
+
const startTime = Date.now();
|
|
318
|
+
this.initPromise = this.loadWasm();
|
|
319
|
+
try {
|
|
320
|
+
await this.initPromise;
|
|
321
|
+
this.initialized = true;
|
|
322
|
+
this.log("info", "WASM module initialized", {
|
|
323
|
+
elapsedMs: Date.now() - startTime,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
this.initPromise = null;
|
|
328
|
+
this.log("error", "WASM initialization failed", {
|
|
329
|
+
error: error instanceof Error ? error.message : String(error),
|
|
330
|
+
});
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async loadWasm() {
|
|
335
|
+
// Load wasm_exec.js if Go is not defined
|
|
336
|
+
if (typeof Go === "undefined") {
|
|
337
|
+
await this.loadWasmExec();
|
|
338
|
+
}
|
|
339
|
+
this.goInstance = new Go();
|
|
340
|
+
try {
|
|
341
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
342
|
+
// Node.js environment
|
|
343
|
+
const fs = await import("fs");
|
|
344
|
+
const wasmBuffer = fs.readFileSync(this.wasmPath);
|
|
345
|
+
const wasmModule = await WebAssembly.compile(wasmBuffer);
|
|
346
|
+
this.wasmInstance = await WebAssembly.instantiate(wasmModule, this.goInstance.importObject);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
// Browser environment
|
|
350
|
+
const result = await WebAssembly.instantiateStreaming(fetch(this.wasmPath), this.goInstance.importObject).catch(async () => {
|
|
351
|
+
const response = await fetch(this.wasmPath);
|
|
352
|
+
if (!response.ok) {
|
|
353
|
+
throw new WasmLoadError(`Failed to fetch WASM file: ${response.status} ${response.statusText}`);
|
|
354
|
+
}
|
|
355
|
+
const bytes = await response.arrayBuffer();
|
|
356
|
+
return WebAssembly.instantiate(bytes, this.goInstance.importObject);
|
|
357
|
+
});
|
|
358
|
+
this.wasmInstance = result.instance;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
throw new WasmLoadError(`Failed to load WASM module: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined);
|
|
363
|
+
}
|
|
364
|
+
// Don't await - Go.run() blocks until the program exits
|
|
365
|
+
this.goInstance.run(this.wasmInstance);
|
|
366
|
+
// Wait for functions to be available
|
|
367
|
+
await this.waitForWasmFunctions();
|
|
368
|
+
// Set debug mode if already enabled
|
|
369
|
+
if (this.debugOptions.enabled &&
|
|
370
|
+
typeof globalThis.jwwSetDebug === "function") {
|
|
371
|
+
globalThis.jwwSetDebug(true);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async loadWasmExec() {
|
|
375
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
376
|
+
const wasmExecPath = new URL("../wasm/wasm_exec.js", import.meta.url)
|
|
377
|
+
.pathname;
|
|
378
|
+
await import(wasmExecPath);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
throw new WasmLoadError("Go runtime not loaded. Please include wasm_exec.js in your HTML before using the parser.");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async waitForWasmFunctions(timeout = 5000, interval = 50) {
|
|
385
|
+
const start = Date.now();
|
|
386
|
+
while (Date.now() - start < timeout) {
|
|
387
|
+
if (typeof globalThis.jwwParse === "function" &&
|
|
388
|
+
typeof globalThis.jwwToDxf === "function" &&
|
|
389
|
+
typeof globalThis.jwwToDxfString === "function") {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
393
|
+
}
|
|
394
|
+
throw new JwwParserError(JwwErrorCode.WASM_TIMEOUT, "WASM functions not available after timeout. The WASM module may have failed to initialize.");
|
|
395
|
+
}
|
|
396
|
+
ensureInitialized() {
|
|
397
|
+
if (!this.initialized) {
|
|
398
|
+
throw new NotInitializedError();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// ===========================================================================
|
|
402
|
+
// Validation Methods
|
|
403
|
+
// ===========================================================================
|
|
404
|
+
/**
|
|
405
|
+
* Validate a JWW file without fully parsing it
|
|
406
|
+
* Useful for checking file validity before processing
|
|
407
|
+
*
|
|
408
|
+
* @param data - JWW file content as Uint8Array
|
|
409
|
+
* @returns Validation result with any issues found
|
|
410
|
+
*/
|
|
411
|
+
validate(data) {
|
|
412
|
+
const startTime = Date.now();
|
|
413
|
+
// Basic validation without WASM (always available)
|
|
414
|
+
const issues = [];
|
|
415
|
+
// Check minimum size
|
|
416
|
+
if (data.length < 16) {
|
|
417
|
+
issues.push({
|
|
418
|
+
severity: "error",
|
|
419
|
+
code: "FILE_TOO_SMALL",
|
|
420
|
+
message: "File is too small to be a valid JWW file",
|
|
421
|
+
details: { size: data.length, minimumRequired: 16 },
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
// Check JWW signature "JwwData."
|
|
425
|
+
const signature = new TextDecoder().decode(data.slice(0, 8));
|
|
426
|
+
if (signature !== "JwwData.") {
|
|
427
|
+
issues.push({
|
|
428
|
+
severity: "error",
|
|
429
|
+
code: "INVALID_SIGNATURE",
|
|
430
|
+
message: `Invalid file signature: expected "JwwData.", got "${signature.replace(/[^\x20-\x7E]/g, "?")}"`,
|
|
431
|
+
offset: 0,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
// Try to read version (bytes 8-11, little-endian)
|
|
435
|
+
let version;
|
|
436
|
+
if (data.length >= 12) {
|
|
437
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
438
|
+
version = view.getInt32(8, true);
|
|
439
|
+
if (version < 200 || version > 1000) {
|
|
440
|
+
issues.push({
|
|
441
|
+
severity: "warning",
|
|
442
|
+
code: "UNUSUAL_VERSION",
|
|
443
|
+
message: `Unusual version number: ${version}. Expected between 200-1000.`,
|
|
444
|
+
details: { version },
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Estimate entity count based on file size (rough heuristic)
|
|
449
|
+
// Average entity is approximately 50-100 bytes
|
|
450
|
+
const estimatedEntityCount = Math.floor(data.length / 75);
|
|
451
|
+
// Size warnings
|
|
452
|
+
const sizeCategory = getSizeCategory(data.length);
|
|
453
|
+
if (sizeCategory === "very_large") {
|
|
454
|
+
issues.push({
|
|
455
|
+
severity: "warning",
|
|
456
|
+
code: "LARGE_FILE",
|
|
457
|
+
message: `File is very large (${formatBytes(data.length)}). Consider using streaming mode for better performance.`,
|
|
458
|
+
details: { size: data.length, sizeFormatted: formatBytes(data.length) },
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
const hasErrors = issues.some((i) => i.severity === "error");
|
|
462
|
+
return {
|
|
463
|
+
valid: !hasErrors,
|
|
464
|
+
version,
|
|
465
|
+
sizeCategory,
|
|
466
|
+
estimatedEntityCount,
|
|
467
|
+
issues,
|
|
468
|
+
validationTimeMs: Date.now() - startTime,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Validate and throw if invalid
|
|
473
|
+
* Convenience method that throws a ValidationError if the file is invalid
|
|
474
|
+
*
|
|
475
|
+
* @param data - JWW file content as Uint8Array
|
|
476
|
+
* @throws {ValidationError} If the file is invalid
|
|
477
|
+
*/
|
|
478
|
+
validateOrThrow(data) {
|
|
479
|
+
const result = this.validate(data);
|
|
480
|
+
if (!result.valid) {
|
|
481
|
+
throw new ValidationError("File validation failed: " +
|
|
482
|
+
result.issues
|
|
483
|
+
.filter((i) => i.severity === "error")
|
|
484
|
+
.map((i) => i.message)
|
|
485
|
+
.join("; "), result.issues);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// ===========================================================================
|
|
489
|
+
// Parsing Methods
|
|
490
|
+
// ===========================================================================
|
|
491
|
+
/**
|
|
492
|
+
* Parse a JWW file and return the document structure
|
|
493
|
+
*
|
|
494
|
+
* @param data - JWW file content as Uint8Array
|
|
495
|
+
* @param options - Optional parsing options
|
|
496
|
+
* @returns Parsed JWW document
|
|
497
|
+
* @throws {NotInitializedError} If parser is not initialized
|
|
498
|
+
* @throws {ParseError} If parsing fails
|
|
499
|
+
*/
|
|
500
|
+
parse(data, options) {
|
|
501
|
+
this.ensureInitialized();
|
|
502
|
+
// Check for abort signal
|
|
503
|
+
if (options?.signal?.aborted) {
|
|
504
|
+
throw new JwwParserError(JwwErrorCode.PARSE_ERROR, "Parse operation was aborted");
|
|
505
|
+
}
|
|
506
|
+
const startTime = Date.now();
|
|
507
|
+
this.log("info", "Starting parse operation", {
|
|
508
|
+
dataSize: data.length,
|
|
509
|
+
options: options ? { ...options, onProgress: undefined } : undefined,
|
|
510
|
+
});
|
|
511
|
+
// Report initial progress
|
|
512
|
+
if (options?.onProgress) {
|
|
513
|
+
options.onProgress({
|
|
514
|
+
stage: "loading",
|
|
515
|
+
progress: 0,
|
|
516
|
+
overallProgress: 0,
|
|
517
|
+
message: "Loading file data",
|
|
518
|
+
elapsedMs: 0,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
const result = globalThis.jwwParse(data);
|
|
523
|
+
if (!result.ok) {
|
|
524
|
+
this.stats.errorCount++;
|
|
525
|
+
throw createParseError(result);
|
|
526
|
+
}
|
|
527
|
+
// Report parsing complete
|
|
528
|
+
if (options?.onProgress) {
|
|
529
|
+
options.onProgress({
|
|
530
|
+
stage: "complete",
|
|
531
|
+
progress: 100,
|
|
532
|
+
overallProgress: 100,
|
|
533
|
+
message: "Parsing complete",
|
|
534
|
+
elapsedMs: Date.now() - startTime,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
const doc = JSON.parse(result.data);
|
|
538
|
+
// Apply filtering options
|
|
539
|
+
let filteredDoc = doc;
|
|
540
|
+
if (options) {
|
|
541
|
+
filteredDoc = this.applyParseOptions(doc, options);
|
|
542
|
+
}
|
|
543
|
+
// Update stats
|
|
544
|
+
const parseTime = Date.now() - startTime;
|
|
545
|
+
this.stats.parseCount++;
|
|
546
|
+
this.stats.totalBytesProcessed += data.length;
|
|
547
|
+
this.stats.parseTimes.push(parseTime);
|
|
548
|
+
if (this.stats.parseTimes.length > 100) {
|
|
549
|
+
this.stats.parseTimes.shift();
|
|
550
|
+
}
|
|
551
|
+
this.log("info", "Parse complete", {
|
|
552
|
+
parseTimeMs: parseTime,
|
|
553
|
+
entityCount: filteredDoc.Entities.length,
|
|
554
|
+
blockCount: filteredDoc.Blocks.length,
|
|
555
|
+
});
|
|
556
|
+
return filteredDoc;
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
this.stats.errorCount++;
|
|
560
|
+
this.log("error", "Parse failed", {
|
|
561
|
+
error: error instanceof Error ? error.message : String(error),
|
|
562
|
+
});
|
|
563
|
+
if (error instanceof JwwParserError) {
|
|
564
|
+
throw error;
|
|
565
|
+
}
|
|
566
|
+
throw new ParseError(error instanceof Error ? error.message : String(error), { cause: error instanceof Error ? error : undefined });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
applyParseOptions(doc, options) {
|
|
570
|
+
let entities = doc.Entities;
|
|
571
|
+
let blocks = doc.Blocks;
|
|
572
|
+
// Filter by entity type
|
|
573
|
+
if (options.skipEntityTypes && options.skipEntityTypes.length > 0) {
|
|
574
|
+
const skipTypes = new Set(options.skipEntityTypes);
|
|
575
|
+
entities = entities.filter((e) => !skipTypes.has(e.Type));
|
|
576
|
+
}
|
|
577
|
+
// Filter by layer group
|
|
578
|
+
if (options.layerGroupFilter && options.layerGroupFilter.length > 0) {
|
|
579
|
+
const groups = new Set(options.layerGroupFilter);
|
|
580
|
+
entities = entities.filter((e) => groups.has(e.LayerGroup));
|
|
581
|
+
}
|
|
582
|
+
// Filter by specific layers
|
|
583
|
+
if (options.layerFilter) {
|
|
584
|
+
const filter = options.layerFilter;
|
|
585
|
+
entities = entities.filter((e) => {
|
|
586
|
+
const layers = filter[e.LayerGroup];
|
|
587
|
+
return !layers || layers.includes(e.Layer);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
// Limit entity count
|
|
591
|
+
if (options.maxEntities && entities.length > options.maxEntities) {
|
|
592
|
+
entities = entities.slice(0, options.maxEntities);
|
|
593
|
+
}
|
|
594
|
+
// Handle blocks
|
|
595
|
+
if (options.includeBlocks === false) {
|
|
596
|
+
blocks = [];
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
...doc,
|
|
600
|
+
Entities: entities,
|
|
601
|
+
Blocks: blocks,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Parse a JWW file and convert to DXF document structure
|
|
606
|
+
*
|
|
607
|
+
* @param data - JWW file content as Uint8Array
|
|
608
|
+
* @param options - Optional conversion options
|
|
609
|
+
* @returns DXF document object
|
|
610
|
+
*/
|
|
611
|
+
toDxf(data, options) {
|
|
612
|
+
this.ensureInitialized();
|
|
613
|
+
const startTime = Date.now();
|
|
614
|
+
this.log("info", "Starting DXF conversion", { dataSize: data.length });
|
|
615
|
+
if (options?.onProgress) {
|
|
616
|
+
options.onProgress({
|
|
617
|
+
stage: "converting",
|
|
618
|
+
progress: 0,
|
|
619
|
+
overallProgress: 0,
|
|
620
|
+
message: "Converting to DXF",
|
|
621
|
+
elapsedMs: 0,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
const result = globalThis.jwwToDxf(data);
|
|
626
|
+
if (!result.ok) {
|
|
627
|
+
this.stats.errorCount++;
|
|
628
|
+
throw createParseError(result);
|
|
629
|
+
}
|
|
630
|
+
if (options?.onProgress) {
|
|
631
|
+
options.onProgress({
|
|
632
|
+
stage: "complete",
|
|
633
|
+
progress: 100,
|
|
634
|
+
overallProgress: 100,
|
|
635
|
+
message: "Conversion complete",
|
|
636
|
+
elapsedMs: Date.now() - startTime,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
const doc = JSON.parse(result.data);
|
|
640
|
+
// Update stats
|
|
641
|
+
const parseTime = Date.now() - startTime;
|
|
642
|
+
this.stats.parseCount++;
|
|
643
|
+
this.stats.totalBytesProcessed += data.length;
|
|
644
|
+
this.stats.parseTimes.push(parseTime);
|
|
645
|
+
this.log("info", "DXF conversion complete", {
|
|
646
|
+
parseTimeMs: parseTime,
|
|
647
|
+
entityCount: doc.Entities.length,
|
|
648
|
+
});
|
|
649
|
+
return doc;
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
this.stats.errorCount++;
|
|
653
|
+
this.log("error", "DXF conversion failed", {
|
|
654
|
+
error: error instanceof Error ? error.message : String(error),
|
|
655
|
+
});
|
|
656
|
+
if (error instanceof JwwParserError) {
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
throw new ParseError(error instanceof Error ? error.message : String(error), { cause: error instanceof Error ? error : undefined });
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Parse a JWW file and convert to DXF file content string
|
|
664
|
+
*
|
|
665
|
+
* @param data - JWW file content as Uint8Array
|
|
666
|
+
* @param options - Optional conversion options
|
|
667
|
+
* @returns DXF file content as string (ready to save as .dxf file)
|
|
668
|
+
*/
|
|
669
|
+
toDxfString(data, options) {
|
|
670
|
+
this.ensureInitialized();
|
|
671
|
+
const startTime = Date.now();
|
|
672
|
+
this.log("info", "Starting DXF string generation", { dataSize: data.length });
|
|
673
|
+
try {
|
|
674
|
+
const result = globalThis.jwwToDxfString(data);
|
|
675
|
+
if (!result.ok) {
|
|
676
|
+
this.stats.errorCount++;
|
|
677
|
+
throw createParseError(result);
|
|
678
|
+
}
|
|
679
|
+
const parseTime = Date.now() - startTime;
|
|
680
|
+
this.stats.parseCount++;
|
|
681
|
+
this.stats.totalBytesProcessed += data.length;
|
|
682
|
+
this.stats.parseTimes.push(parseTime);
|
|
683
|
+
this.log("info", "DXF string generation complete", {
|
|
684
|
+
parseTimeMs: parseTime,
|
|
685
|
+
outputLength: result.data.length,
|
|
686
|
+
});
|
|
687
|
+
return result.data;
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
this.stats.errorCount++;
|
|
691
|
+
if (error instanceof JwwParserError) {
|
|
692
|
+
throw error;
|
|
693
|
+
}
|
|
694
|
+
throw new ParseError(error instanceof Error ? error.message : String(error), { cause: error instanceof Error ? error : undefined });
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// ===========================================================================
|
|
698
|
+
// Memory Management
|
|
699
|
+
// ===========================================================================
|
|
700
|
+
/**
|
|
701
|
+
* Get current memory usage statistics
|
|
702
|
+
*/
|
|
703
|
+
getMemoryStats() {
|
|
704
|
+
let wasmMemoryBytes = 0;
|
|
705
|
+
// Try to get WASM memory size
|
|
706
|
+
if (this.wasmInstance?.exports.memory) {
|
|
707
|
+
const memory = this.wasmInstance.exports.memory;
|
|
708
|
+
wasmMemoryBytes = memory.buffer.byteLength;
|
|
709
|
+
}
|
|
710
|
+
// Try to get JS heap size (Node.js only)
|
|
711
|
+
let jsHeapBytes;
|
|
712
|
+
if (typeof process !== "undefined" &&
|
|
713
|
+
typeof process.memoryUsage === "function") {
|
|
714
|
+
jsHeapBytes = process.memoryUsage().heapUsed;
|
|
715
|
+
}
|
|
716
|
+
const totalBytes = wasmMemoryBytes + (jsHeapBytes || 0);
|
|
717
|
+
return {
|
|
718
|
+
wasmMemoryBytes,
|
|
719
|
+
jsHeapBytes,
|
|
720
|
+
totalBytes,
|
|
721
|
+
totalFormatted: formatBytes(totalBytes),
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Get parser statistics
|
|
726
|
+
*/
|
|
727
|
+
getStats() {
|
|
728
|
+
const parseTimes = this.stats.parseTimes;
|
|
729
|
+
return {
|
|
730
|
+
parseCount: this.stats.parseCount,
|
|
731
|
+
totalBytesProcessed: this.stats.totalBytesProcessed,
|
|
732
|
+
averageParseTimeMs: parseTimes.length > 0
|
|
733
|
+
? parseTimes.reduce((a, b) => a + b, 0) / parseTimes.length
|
|
734
|
+
: 0,
|
|
735
|
+
fastestParseTimeMs: parseTimes.length > 0 ? Math.min(...parseTimes) : 0,
|
|
736
|
+
slowestParseTimeMs: parseTimes.length > 0 ? Math.max(...parseTimes) : 0,
|
|
737
|
+
errorCount: this.stats.errorCount,
|
|
738
|
+
memoryStats: this.getMemoryStats(),
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Reset parser statistics
|
|
743
|
+
*/
|
|
744
|
+
resetStats() {
|
|
745
|
+
this.stats = {
|
|
746
|
+
parseCount: 0,
|
|
747
|
+
totalBytesProcessed: 0,
|
|
748
|
+
parseTimes: [],
|
|
749
|
+
errorCount: 0,
|
|
750
|
+
};
|
|
751
|
+
this.log("debug", "Statistics reset");
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Clean up resources and release memory
|
|
755
|
+
* Call this when you're done using the parser to free WASM memory
|
|
756
|
+
*/
|
|
757
|
+
dispose() {
|
|
758
|
+
this.log("info", "Disposing parser resources");
|
|
759
|
+
// Clear references
|
|
760
|
+
this.goInstance = null;
|
|
761
|
+
this.wasmInstance = null;
|
|
762
|
+
this.initialized = false;
|
|
763
|
+
this.initPromise = null;
|
|
764
|
+
// Clear stats and logs
|
|
765
|
+
this.stats = {
|
|
766
|
+
parseCount: 0,
|
|
767
|
+
totalBytesProcessed: 0,
|
|
768
|
+
parseTimes: [],
|
|
769
|
+
errorCount: 0,
|
|
770
|
+
};
|
|
771
|
+
this.debugLogs = [];
|
|
772
|
+
// Note: We cannot truly "free" WASM memory, but clearing references
|
|
773
|
+
// allows garbage collection to reclaim the memory
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Get the WASM module version
|
|
777
|
+
*/
|
|
778
|
+
getVersion() {
|
|
779
|
+
if (typeof globalThis.jwwGetVersion === "function") {
|
|
780
|
+
return globalThis.jwwGetVersion();
|
|
781
|
+
}
|
|
782
|
+
return "1.0.0"; // Fallback version
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// =============================================================================
|
|
786
|
+
// Factory Functions
|
|
787
|
+
// =============================================================================
|
|
788
|
+
/**
|
|
789
|
+
* Create and initialize a JWW parser instance
|
|
790
|
+
*
|
|
791
|
+
* @param wasmPath - Optional path to the jww-parser.wasm file
|
|
792
|
+
* @param options - Optional debug options
|
|
793
|
+
* @returns Initialized JwwParser instance
|
|
794
|
+
*
|
|
795
|
+
* @example
|
|
796
|
+
* ```typescript
|
|
797
|
+
* const parser = await createParser();
|
|
798
|
+
* const doc = parser.parse(fileData);
|
|
799
|
+
* ```
|
|
800
|
+
*/
|
|
801
|
+
export async function createParser(wasmPath, options) {
|
|
802
|
+
const parser = new JwwParser(wasmPath);
|
|
803
|
+
if (options?.debug) {
|
|
804
|
+
parser.setDebug(options.debug);
|
|
805
|
+
}
|
|
806
|
+
await parser.init();
|
|
807
|
+
return parser;
|
|
161
808
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
809
|
+
/**
|
|
810
|
+
* Quick validate a JWW file without initializing a full parser
|
|
811
|
+
* Performs basic validation only (signature, version check)
|
|
812
|
+
*
|
|
813
|
+
* @param data - JWW file content as Uint8Array
|
|
814
|
+
* @returns Validation result
|
|
815
|
+
*/
|
|
816
|
+
export function quickValidate(data) {
|
|
817
|
+
const parser = new JwwParser();
|
|
818
|
+
return parser.validate(data);
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Check if a Uint8Array looks like a JWW file
|
|
822
|
+
*
|
|
823
|
+
* @param data - File content as Uint8Array
|
|
824
|
+
* @returns true if the file appears to be a JWW file
|
|
825
|
+
*/
|
|
826
|
+
export function isJwwFile(data) {
|
|
827
|
+
if (data.length < 8)
|
|
828
|
+
return false;
|
|
829
|
+
const signature = new TextDecoder().decode(data.slice(0, 8));
|
|
830
|
+
return signature === "JwwData.";
|
|
831
|
+
}
|
|
832
|
+
// =============================================================================
|
|
833
|
+
// Default Export
|
|
834
|
+
// =============================================================================
|
|
835
|
+
export default {
|
|
836
|
+
JwwParser,
|
|
837
|
+
createParser,
|
|
838
|
+
quickValidate,
|
|
839
|
+
isJwwFile,
|
|
840
|
+
JwwParserError,
|
|
841
|
+
NotInitializedError,
|
|
842
|
+
WasmLoadError,
|
|
843
|
+
ValidationError,
|
|
844
|
+
ParseError,
|
|
845
|
+
JwwErrorCode,
|
|
846
|
+
};
|