glost-processor 0.5.0

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/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "glost-processor",
3
+ "version": "0.5.0",
4
+ "description": "Unified-style processor API for GLOST documents",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "README.md"
18
+ ],
19
+ "keywords": [
20
+ "glost",
21
+ "processor",
22
+ "pipeline",
23
+ "unified",
24
+ "ast"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "glost-core": "0.5.0",
30
+ "glost-extensions": "0.4.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20.0.0",
34
+ "typescript": "^5.3.3",
35
+ "vitest": "^1.6.0"
36
+ },
37
+ "peerDependencies": {
38
+ "glost-core": "^0.5.0"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc",
42
+ "test": "vitest run --passWithNoTests",
43
+ "test:watch": "vitest"
44
+ }
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * GLOST Processor
3
+ *
4
+ * Unified-style processor for GLOST documents.
5
+ *
6
+ * @packageDocumentation
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { glost } from "glost-processor";
11
+ * import { transcription } from "glost-transcription";
12
+ * import { translation } from "glost-translation";
13
+ *
14
+ * const processor = glost()
15
+ * .use(transcription, { scheme: "ipa" })
16
+ * .use(translation, { target: "en" })
17
+ * .freeze();
18
+ *
19
+ * const result = await processor.process(document);
20
+ * ```
21
+ */
22
+
23
+ export { GLOSTProcessor } from "./processor.js";
24
+ export type { FrozenProcessor } from "./processor.js";
25
+ export type {
26
+ Plugin,
27
+ PluginSpec,
28
+ Preset,
29
+ ProcessorOptions,
30
+ ProcessingResult,
31
+ ProcessingError,
32
+ ProcessingWarning,
33
+ ProcessingStats,
34
+ BeforeHook,
35
+ AfterHook,
36
+ ErrorHook,
37
+ SkipHook,
38
+ ProgressHook,
39
+ ProgressStats,
40
+ } from "./types.js";
41
+
42
+ import { GLOSTProcessor } from "./processor.js";
43
+ import type { ProcessorOptions } from "./types.js";
44
+
45
+ /**
46
+ * Create a new GLOST processor
47
+ *
48
+ * Factory function for creating a new processor instance.
49
+ * Similar to `unified()` from the unified ecosystem.
50
+ *
51
+ * @param options - Initial processor options
52
+ * @returns A new processor instance
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * import { glost } from "glost-processor";
57
+ *
58
+ * const processor = glost()
59
+ * .use(plugin1)
60
+ * .use(plugin2)
61
+ * .use(plugin3);
62
+ *
63
+ * const result = await processor.process(document);
64
+ * ```
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * // With options
69
+ * const processor = glost({ lenient: true, debug: true })
70
+ * .use(plugin1)
71
+ * .use(plugin2);
72
+ * ```
73
+ */
74
+ export function glost(options?: ProcessorOptions): GLOSTProcessor {
75
+ return new GLOSTProcessor(options);
76
+ }
@@ -0,0 +1,574 @@
1
+ /**
2
+ * GLOST Processor
3
+ *
4
+ * Unified-style processor for GLOST documents with fluent API.
5
+ * Similar to unified/remark processors.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import type { GLOSTRoot } from "glost-core";
11
+ import type { GLOSTExtension } from "glost-extensions";
12
+ import { processGLOSTWithExtensionsAsync, extensionRegistry } from "glost-extensions";
13
+ import type {
14
+ PluginSpec,
15
+ Preset,
16
+ ProcessorOptions,
17
+ ProcessingResult,
18
+ ProcessorHooks,
19
+ BeforeHook,
20
+ AfterHook,
21
+ ErrorHook,
22
+ SkipHook,
23
+ ProgressHook,
24
+ ProcessingError,
25
+ ProcessingWarning,
26
+ ProgressStats,
27
+ } from "./types.js";
28
+
29
+ /**
30
+ * GLOST Processor
31
+ *
32
+ * Fluent API for processing GLOST documents through plugin pipelines.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * import { GLOSTProcessor } from "glost-processor";
37
+ *
38
+ * const processor = new GLOSTProcessor()
39
+ * .use(transcription, { scheme: "ipa" })
40
+ * .use(translation, { target: "en" })
41
+ * .use(frequency);
42
+ *
43
+ * const result = await processor.process(document);
44
+ * ```
45
+ */
46
+ export class GLOSTProcessor {
47
+ private plugins: Array<{ spec: PluginSpec; options?: any }> = [];
48
+ private hooks: ProcessorHooks = {
49
+ before: new Map(),
50
+ after: new Map(),
51
+ onError: [],
52
+ onSkip: [],
53
+ onProgress: [],
54
+ };
55
+ private dataStore: Map<string, any> = new Map();
56
+ private options: ProcessorOptions = {};
57
+ private frozen = false;
58
+
59
+ /**
60
+ * Create a new processor instance
61
+ *
62
+ * @param options - Initial processor options
63
+ */
64
+ constructor(options: ProcessorOptions = {}) {
65
+ this.options = { ...options };
66
+ if (options.data) {
67
+ this.dataStore = new Map(options.data);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Use a plugin, preset, or extension
73
+ *
74
+ * @param spec - Plugin function, extension object, preset, or plugin ID
75
+ * @param options - Plugin options
76
+ * @returns This processor for chaining
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * processor
81
+ * .use(transcription, { scheme: "ipa" })
82
+ * .use(translation)
83
+ * .use("frequency");
84
+ * ```
85
+ */
86
+ use(spec: PluginSpec | Preset, options?: any): this {
87
+ this.assertNotFrozen();
88
+
89
+ // Handle presets
90
+ if (this.isPreset(spec)) {
91
+ return this.usePreset(spec);
92
+ }
93
+
94
+ // Add plugin to pipeline
95
+ this.plugins.push({ spec, options });
96
+ return this;
97
+ }
98
+
99
+ /**
100
+ * Apply a preset (collection of plugins)
101
+ *
102
+ * @param preset - Preset to apply
103
+ * @returns This processor for chaining
104
+ */
105
+ private usePreset(preset: Preset): this {
106
+ for (const pluginEntry of preset.plugins) {
107
+ if (Array.isArray(pluginEntry)) {
108
+ const [plugin, opts] = pluginEntry;
109
+ this.use(plugin, opts);
110
+ } else {
111
+ this.use(pluginEntry);
112
+ }
113
+ }
114
+ return this;
115
+ }
116
+
117
+ /**
118
+ * Register a hook to run before a plugin
119
+ *
120
+ * @param pluginId - Plugin ID to hook into
121
+ * @param hook - Hook function to run
122
+ * @returns This processor for chaining
123
+ *
124
+ * @example
125
+ * ```typescript
126
+ * processor.before("translation", (doc) => {
127
+ * console.log("About to translate");
128
+ * });
129
+ * ```
130
+ */
131
+ before(pluginId: string, hook: BeforeHook): this {
132
+ this.assertNotFrozen();
133
+ if (!this.hooks.before.has(pluginId)) {
134
+ this.hooks.before.set(pluginId, []);
135
+ }
136
+ this.hooks.before.get(pluginId)!.push(hook);
137
+ return this;
138
+ }
139
+
140
+ /**
141
+ * Register a hook to run after a plugin
142
+ *
143
+ * @param pluginId - Plugin ID to hook into
144
+ * @param hook - Hook function to run
145
+ * @returns This processor for chaining
146
+ *
147
+ * @example
148
+ * ```typescript
149
+ * processor.after("translation", (doc) => {
150
+ * console.log("Translation complete");
151
+ * });
152
+ * ```
153
+ */
154
+ after(pluginId: string, hook: AfterHook): this {
155
+ this.assertNotFrozen();
156
+ if (!this.hooks.after.has(pluginId)) {
157
+ this.hooks.after.set(pluginId, []);
158
+ }
159
+ this.hooks.after.get(pluginId)!.push(hook);
160
+ return this;
161
+ }
162
+
163
+ /**
164
+ * Register an error handler
165
+ *
166
+ * @param hook - Error handler function
167
+ * @returns This processor for chaining
168
+ *
169
+ * @example
170
+ * ```typescript
171
+ * processor.onError((error, plugin) => {
172
+ * console.error(`Plugin ${plugin} failed:`, error);
173
+ * });
174
+ * ```
175
+ */
176
+ onError(hook: ErrorHook): this {
177
+ this.assertNotFrozen();
178
+ this.hooks.onError.push(hook);
179
+ return this;
180
+ }
181
+
182
+ /**
183
+ * Register a skip handler
184
+ *
185
+ * @param hook - Skip handler function
186
+ * @returns This processor for chaining
187
+ *
188
+ * @example
189
+ * ```typescript
190
+ * processor.onSkip((plugin, reason) => {
191
+ * console.log(`Plugin ${plugin} skipped: ${reason}`);
192
+ * });
193
+ * ```
194
+ */
195
+ onSkip(hook: SkipHook): this {
196
+ this.assertNotFrozen();
197
+ this.hooks.onSkip.push(hook);
198
+ return this;
199
+ }
200
+
201
+ /**
202
+ * Register a progress handler
203
+ *
204
+ * @param hook - Progress handler function
205
+ * @returns This processor for chaining
206
+ *
207
+ * @example
208
+ * ```typescript
209
+ * processor.onProgress((stats) => {
210
+ * console.log(`Progress: ${stats.completed}/${stats.total}`);
211
+ * });
212
+ * ```
213
+ */
214
+ onProgress(hook: ProgressHook): this {
215
+ this.assertNotFrozen();
216
+ this.hooks.onProgress.push(hook);
217
+ return this;
218
+ }
219
+
220
+ /**
221
+ * Set or get data in the processor data store
222
+ *
223
+ * @param key - Data key
224
+ * @param value - Data value (omit to get)
225
+ * @returns Value if getting, this processor if setting
226
+ *
227
+ * @example
228
+ * ```typescript
229
+ * processor.data("config", { theme: "dark" });
230
+ * const config = processor.data("config");
231
+ * ```
232
+ */
233
+ data(key: string): any;
234
+ data(key: string, value: any): this;
235
+ data(key: string, value?: any): any {
236
+ if (arguments.length === 1) {
237
+ return this.dataStore.get(key);
238
+ }
239
+ this.assertNotFrozen();
240
+ this.dataStore.set(key, value);
241
+ return this;
242
+ }
243
+
244
+ /**
245
+ * Freeze the processor
246
+ *
247
+ * Returns a frozen processor that cannot be modified.
248
+ * Useful for reusing the same configuration across multiple documents.
249
+ *
250
+ * @returns A frozen copy of this processor
251
+ *
252
+ * @example
253
+ * ```typescript
254
+ * const frozen = processor
255
+ * .use(transcription)
256
+ * .use(translation)
257
+ * .freeze();
258
+ *
259
+ * // Can process multiple documents with the same pipeline
260
+ * const result1 = await frozen.process(doc1);
261
+ * const result2 = await frozen.process(doc2);
262
+ * ```
263
+ */
264
+ freeze(): FrozenProcessor {
265
+ const frozen = new GLOSTProcessor(this.options);
266
+ frozen.plugins = [...this.plugins];
267
+ frozen.hooks = {
268
+ before: new Map(this.hooks.before),
269
+ after: new Map(this.hooks.after),
270
+ onError: [...this.hooks.onError],
271
+ onSkip: [...this.hooks.onSkip],
272
+ onProgress: [...this.hooks.onProgress],
273
+ };
274
+ frozen.dataStore = new Map(this.dataStore);
275
+ (frozen as any).frozen = true;
276
+ return frozen as unknown as FrozenProcessor;
277
+ }
278
+
279
+ /**
280
+ * Process a document through the pipeline
281
+ *
282
+ * @param document - GLOST document to process
283
+ * @returns Promise resolving to the processed document
284
+ *
285
+ * @example
286
+ * ```typescript
287
+ * const result = await processor.process(document);
288
+ * console.log(result);
289
+ * ```
290
+ */
291
+ async process(document: GLOSTRoot): Promise<GLOSTRoot> {
292
+ const result = await this.processWithMeta(document);
293
+ return result.document;
294
+ }
295
+
296
+ /**
297
+ * Process a document and return detailed metadata
298
+ *
299
+ * @param document - GLOST document to process
300
+ * @returns Promise resolving to processing result with metadata
301
+ *
302
+ * @example
303
+ * ```typescript
304
+ * const result = await processor.processWithMeta(document);
305
+ * console.log(result.metadata.appliedPlugins);
306
+ * console.log(result.metadata.stats.totalTime);
307
+ * ```
308
+ */
309
+ async processWithMeta(document: GLOSTRoot): Promise<ProcessingResult> {
310
+ const startTime = Date.now();
311
+ const extensions = await this.resolveExtensions();
312
+ const timing = new Map<string, number>();
313
+ const errors: ProcessingError[] = [];
314
+ const warnings: ProcessingWarning[] = [];
315
+ const appliedPlugins: string[] = [];
316
+ const skippedPlugins: string[] = [];
317
+
318
+ // Emit initial progress
319
+ this.emitProgress({
320
+ total: extensions.length,
321
+ completed: 0,
322
+ startTime,
323
+ elapsed: 0,
324
+ });
325
+
326
+ // Process extensions with hooks
327
+ let processedDoc = document;
328
+
329
+ for (let i = 0; i < extensions.length; i++) {
330
+ const extension = extensions[i]!;
331
+ const pluginStart = Date.now();
332
+
333
+ try {
334
+ // Run before hooks
335
+ await this.runBeforeHooks(processedDoc, extension.id);
336
+
337
+ // Process with this extension
338
+ const { data: _, ...extensionOptions } = this.options as any;
339
+ const result = await processGLOSTWithExtensionsAsync(
340
+ processedDoc,
341
+ [extension],
342
+ extensionOptions
343
+ );
344
+
345
+ processedDoc = result.document;
346
+
347
+ // Check for errors/skips
348
+ if (result.metadata.errors.length > 0) {
349
+ for (const err of result.metadata.errors) {
350
+ errors.push({
351
+ plugin: extension.id,
352
+ phase: "transform",
353
+ message: err.error.message,
354
+ stack: err.error.stack,
355
+ recoverable: true,
356
+ error: err.error,
357
+ });
358
+ }
359
+ }
360
+
361
+ if (result.metadata.skippedExtensions.includes(extension.id)) {
362
+ skippedPlugins.push(extension.id);
363
+ this.emitSkip(extension.id, "Skipped by processor");
364
+ } else {
365
+ appliedPlugins.push(extension.id);
366
+ }
367
+
368
+ // Run after hooks
369
+ await this.runAfterHooks(processedDoc, extension.id);
370
+
371
+ // Record timing
372
+ timing.set(extension.id, Date.now() - pluginStart);
373
+
374
+ // Emit progress
375
+ this.emitProgress({
376
+ total: extensions.length,
377
+ completed: i + 1,
378
+ current: extension.id,
379
+ startTime,
380
+ elapsed: Date.now() - startTime,
381
+ });
382
+ } catch (error) {
383
+ const err = error instanceof Error ? error : new Error(String(error));
384
+ errors.push({
385
+ plugin: extension.id,
386
+ phase: "transform",
387
+ message: err.message,
388
+ stack: err.stack,
389
+ recoverable: false,
390
+ error: err,
391
+ });
392
+ skippedPlugins.push(extension.id);
393
+ this.emitError(err, extension.id);
394
+ this.emitSkip(extension.id, err.message);
395
+
396
+ // Re-throw in strict mode
397
+ if (!this.options.lenient) {
398
+ throw err;
399
+ }
400
+ }
401
+ }
402
+
403
+ const endTime = Date.now();
404
+
405
+ return {
406
+ document: processedDoc,
407
+ metadata: {
408
+ appliedPlugins,
409
+ skippedPlugins,
410
+ errors,
411
+ warnings,
412
+ stats: {
413
+ totalTime: endTime - startTime,
414
+ timing,
415
+ nodesProcessed: 0, // Could be calculated by visiting the tree
416
+ startTime,
417
+ endTime,
418
+ },
419
+ },
420
+ };
421
+ }
422
+
423
+ /**
424
+ * Process a document synchronously (only if all plugins are sync)
425
+ *
426
+ * @param document - GLOST document to process
427
+ * @returns The processed document
428
+ * @throws {Error} If any plugin is async
429
+ *
430
+ * @example
431
+ * ```typescript
432
+ * const result = processor.processSync(document);
433
+ * ```
434
+ */
435
+ processSync(document: GLOSTRoot): GLOSTRoot {
436
+ throw new Error(
437
+ "Synchronous processing not yet implemented. Use process() instead."
438
+ );
439
+ }
440
+
441
+ /**
442
+ * Resolve all plugins to extensions
443
+ */
444
+ private async resolveExtensions(): Promise<GLOSTExtension[]> {
445
+ const extensions: GLOSTExtension[] = [];
446
+
447
+ for (const { spec, options } of this.plugins) {
448
+ const extension = await this.resolvePlugin(spec, options);
449
+ if (extension) {
450
+ extensions.push(extension);
451
+ }
452
+ }
453
+
454
+ return extensions;
455
+ }
456
+
457
+ /**
458
+ * Resolve a single plugin to an extension
459
+ */
460
+ private async resolvePlugin(
461
+ spec: PluginSpec,
462
+ options?: any
463
+ ): Promise<GLOSTExtension | null> {
464
+ // String ID - lookup in registry
465
+ if (typeof spec === "string") {
466
+ const ext = extensionRegistry.get(spec);
467
+ if (!ext) {
468
+ throw new Error(`Plugin "${spec}" not found in registry`);
469
+ }
470
+ return ext;
471
+ }
472
+
473
+ // Function - call to get extension
474
+ if (typeof spec === "function") {
475
+ const result = spec(options);
476
+ return result || null;
477
+ }
478
+
479
+ // Extension object - use directly
480
+ return spec;
481
+ }
482
+
483
+ /**
484
+ * Check if a spec is a preset
485
+ */
486
+ private isPreset(spec: any): spec is Preset {
487
+ return (
488
+ spec &&
489
+ typeof spec === "object" &&
490
+ "plugins" in spec &&
491
+ Array.isArray(spec.plugins)
492
+ );
493
+ }
494
+
495
+ /**
496
+ * Run before hooks for a plugin
497
+ */
498
+ private async runBeforeHooks(document: GLOSTRoot, pluginId: string): Promise<void> {
499
+ const hooks = this.hooks.before.get(pluginId) || [];
500
+ for (const hook of hooks) {
501
+ await hook(document, pluginId);
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Run after hooks for a plugin
507
+ */
508
+ private async runAfterHooks(document: GLOSTRoot, pluginId: string): Promise<void> {
509
+ const hooks = this.hooks.after.get(pluginId) || [];
510
+ for (const hook of hooks) {
511
+ await hook(document, pluginId);
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Emit error to error handlers
517
+ */
518
+ private emitError(error: Error, pluginId: string): void {
519
+ for (const hook of this.hooks.onError) {
520
+ try {
521
+ hook(error, pluginId);
522
+ } catch (err) {
523
+ console.error("Error in error hook:", err);
524
+ }
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Emit skip to skip handlers
530
+ */
531
+ private emitSkip(pluginId: string, reason: string): void {
532
+ for (const hook of this.hooks.onSkip) {
533
+ try {
534
+ hook(pluginId, reason);
535
+ } catch (err) {
536
+ console.error("Error in skip hook:", err);
537
+ }
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Emit progress to progress handlers
543
+ */
544
+ private emitProgress(stats: ProgressStats): void {
545
+ for (const hook of this.hooks.onProgress) {
546
+ try {
547
+ hook(stats);
548
+ } catch (err) {
549
+ console.error("Error in progress hook:", err);
550
+ }
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Assert that the processor is not frozen
556
+ */
557
+ private assertNotFrozen(): void {
558
+ if (this.frozen) {
559
+ throw new Error("Cannot modify frozen processor");
560
+ }
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Frozen processor type
566
+ *
567
+ * A frozen processor cannot be modified, only used for processing.
568
+ */
569
+ export type FrozenProcessor = Omit<
570
+ GLOSTProcessor,
571
+ "use" | "before" | "after" | "onError" | "onSkip" | "onProgress" | "data" | "freeze"
572
+ > & {
573
+ readonly frozen: true;
574
+ };