udpipe-node 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,906 @@
1
+ // cli.ts — Interactive CLI for udpipe-node.
2
+ // Launches via "npm start".
3
+ import { createUDPipe, WasmEngine } from './wasm-engine.js';
4
+ import { UDPipe } from './index.js';
5
+ import chalk from 'chalk';
6
+ import readline from 'node:readline';
7
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { basename } from 'node:path';
9
+ // Temporary diagnostic crash logger
10
+ process.on('uncaughtException', (err) => {
11
+ try {
12
+ writeFileSync('crash.log', 'Uncaught Exception:\n' + (err.stack || err.message) + '\n');
13
+ }
14
+ catch (e) { }
15
+ console.error(chalk.red('\nUncaught Exception:'), err);
16
+ process.stdout.write('\u001B[?25h'); // show cursor
17
+ process.exit(1);
18
+ });
19
+ process.on('unhandledRejection', (reason) => {
20
+ const err = reason instanceof Error ? reason : new Error(String(reason));
21
+ try {
22
+ writeFileSync('crash.log', 'Unhandled Rejection:\n' + (err.stack || err.message) + '\n');
23
+ }
24
+ catch (e) { }
25
+ console.error(chalk.red('\nUnhandled Rejection:'), err);
26
+ process.stdout.write('\u001B[?25h'); // show cursor
27
+ process.exit(1);
28
+ });
29
+ // Initial loading state
30
+ console.log(chalk.bold.yellow('Loading UDPipe WASM module and English model...'));
31
+ let currentModelPath = 'bundled:english-gum-ud-2.5-191206.udpipe';
32
+ let currentModelName = 'English GUM (UD 2.5)';
33
+ let nlp;
34
+ try {
35
+ nlp = createUDPipe();
36
+ }
37
+ catch (e) {
38
+ console.error(chalk.red('Failed to load default WASM engine / model:'), e);
39
+ process.exit(1);
40
+ }
41
+ // POS tagging color mapping based on Penn Treebank XPOS subtypes
42
+ function getXposColor(xpos) {
43
+ if (xpos.startsWith('VB') || xpos === 'MD') {
44
+ return chalk.greenBright; // Verbs
45
+ }
46
+ if (xpos.startsWith('NN')) {
47
+ return chalk.cyanBright; // Nouns
48
+ }
49
+ if (xpos.startsWith('PRP') || xpos.startsWith('WP') || xpos === 'EX') {
50
+ return chalk.cyan; // Pronouns
51
+ }
52
+ if (xpos.startsWith('JJ')) {
53
+ return chalk.yellowBright; // Adjectives
54
+ }
55
+ if (xpos.startsWith('RB') || xpos === 'WRB') {
56
+ return chalk.magentaBright; // Adverbs
57
+ }
58
+ if (xpos === 'CC') {
59
+ return chalk.hex('#FF69B4'); // Coordinating Conjunction (Pink)
60
+ }
61
+ if (xpos === 'IN') {
62
+ return chalk.hex('#FFA500'); // Prepositions / Subordinating Conjunctions (Orange)
63
+ }
64
+ if (xpos === 'DT' || xpos === 'PDT' || xpos === 'WDT') {
65
+ return chalk.gray; // Determiners
66
+ }
67
+ if (xpos === 'CD') {
68
+ return chalk.hex('#8258FA'); // Cardinals
69
+ }
70
+ if (['.', ',', ':', '(', ')', '``', "''"].includes(xpos)) {
71
+ return chalk.dim; // Punctuation
72
+ }
73
+ return chalk.white;
74
+ }
75
+ // Dependency relation color mapping as requested
76
+ function getDeprelColor(deprel) {
77
+ const base = deprel.split(':')[0].toLowerCase();
78
+ // Nominals (Red shades)
79
+ if (['nsubj', 'obj', 'iobj', 'dobj'].includes(base)) {
80
+ return chalk.hex('#FF6B6B'); // Red shade 1 (Nominals Core arguments)
81
+ }
82
+ if (['obl', 'pobj'].includes(base)) {
83
+ return chalk.hex('#FF4757'); // Red shade 2 (Nominals Non-core dependents)
84
+ }
85
+ if (['nmod', 'appos', 'nummod', 'poss'].includes(base)) {
86
+ return chalk.hex('#D63031'); // Red shade 3 (Nominals Nominal dependents)
87
+ }
88
+ // Clauses (Blue shades)
89
+ if (['csubj', 'ccomp', 'xcomp'].includes(base)) {
90
+ return chalk.hex('#54A0FF'); // Blue shade 1 (Clauses Core arguments)
91
+ }
92
+ if (['advcl'].includes(base)) {
93
+ return chalk.hex('#2E86DE'); // Blue shade 2 (Clauses Non-core dependents)
94
+ }
95
+ if (['acl', 'rcmod'].includes(base)) {
96
+ return chalk.hex('#0984E3'); // Blue shade 3 (Clauses Nominal dependents)
97
+ }
98
+ // Modifiers (Green shades)
99
+ if (['advmod', 'neg'].includes(base)) {
100
+ return chalk.hex('#2ECC71'); // Green shade 1 (Modifier Non-core)
101
+ }
102
+ if (['amod', 'det', 'predet', 'quantmod'].includes(base)) {
103
+ return chalk.hex('#27AE60'); // Green shade 2 (Modifier Nominal)
104
+ }
105
+ // Function words (Greys)
106
+ if (['mark', 'cop', 'aux'].includes(base)) {
107
+ return chalk.hex('#A4B0BE'); // Grey shade 1 (Function Words Non-core)
108
+ }
109
+ if (['case', 'clf'].includes(base)) {
110
+ return chalk.hex('#747D8C'); // Grey shade 2 (Function Words Nominal)
111
+ }
112
+ // Pink (Coordination Words)
113
+ if (['cc', 'conj'].includes(base)) {
114
+ return chalk.hex('#FF69B4');
115
+ }
116
+ // Gold (root)
117
+ if (base === 'root') {
118
+ return chalk.hex('#FFD700');
119
+ }
120
+ // Purple (compounds)
121
+ if (['compound', 'flat', 'fixed', 'nn', 'mwe'].includes(base)) {
122
+ return chalk.hex('#A55EEA');
123
+ }
124
+ // Default/remaining
125
+ return chalk.hex('#FF9F43');
126
+ }
127
+ // Print tabular word lists with proper ANSI escape padding alignment
128
+ function printWordTable(words) {
129
+ const header = 'ID'.padEnd(4) +
130
+ 'FORM'.padEnd(16) +
131
+ 'LEMMA'.padEnd(16) +
132
+ 'UPOS'.padEnd(10) +
133
+ 'XPOS'.padEnd(10) +
134
+ 'HEAD'.padEnd(6) +
135
+ 'DEPREL';
136
+ console.log('\n' + chalk.bold.blue(header));
137
+ console.log(chalk.dim('─'.repeat(70)));
138
+ for (const w of words) {
139
+ const wordColor = getXposColor(w.xpos);
140
+ const deprelColor = getDeprelColor(w.deprel);
141
+ const idStr = chalk.dim(String(w.id).padEnd(4));
142
+ // Pad first, then apply color so length calculations ignore ANSI codes
143
+ const formStr = wordColor(w.form.padEnd(16));
144
+ const lemmaStr = chalk.dim((w.lemma || '_').padEnd(16));
145
+ const uposStr = wordColor(w.upos.padEnd(10));
146
+ const xposStr = wordColor(w.xpos.padEnd(10));
147
+ const headStr = chalk.dim(String(w.head).padEnd(6));
148
+ const deprelStr = deprelColor(w.deprel);
149
+ console.log(`${idStr}${formStr}${lemmaStr}${uposStr}${xposStr}${headStr}${deprelStr}`);
150
+ }
151
+ }
152
+ // Print flat directed graph edges list
153
+ function printGraphEdges(words) {
154
+ console.log(chalk.bold.yellow('Directed Graph Edges:'));
155
+ for (const w of words) {
156
+ const childColor = getXposColor(w.xpos);
157
+ const deprelColor = getDeprelColor(w.deprel);
158
+ if (w.head !== 0) {
159
+ const parentWord = words[w.head - 1];
160
+ const parentForm = parentWord ? parentWord.form : '?';
161
+ const parentColor = parentWord ? getXposColor(parentWord.xpos) : chalk.white;
162
+ console.log(` ${parentColor(parentForm)} (${chalk.dim(w.head)}) ──[${deprelColor(w.deprel)}]──> ${childColor(w.form)} (${chalk.dim(w.id)})`);
163
+ }
164
+ else {
165
+ console.log(` ${chalk.bold.hex('#FFD700')('ROOT')} ──[${deprelColor(w.deprel)}]──> ${childColor(w.form)} (${chalk.dim(w.id)})`);
166
+ }
167
+ }
168
+ }
169
+ // Render hierarchical dependency tree diagram using Unicode characters
170
+ function renderDependencyTree(words) {
171
+ const parentToChildren = new Map();
172
+ for (const w of words) {
173
+ if (!parentToChildren.has(w.head)) {
174
+ parentToChildren.set(w.head, []);
175
+ }
176
+ parentToChildren.get(w.head).push(w);
177
+ }
178
+ // Sort children by ID
179
+ for (const children of parentToChildren.values()) {
180
+ children.sort((a, b) => a.id - b.id);
181
+ }
182
+ const roots = parentToChildren.get(0) || [];
183
+ const visited = new Set();
184
+ function printNode(w, prefix, isLast) {
185
+ if (visited.has(w.id)) {
186
+ console.log(`${prefix}${isLast ? '└── ' : '├── '}${chalk.red(`[Cycle detected: ID ${w.id}]`)}`);
187
+ return;
188
+ }
189
+ visited.add(w.id);
190
+ const wordColor = getXposColor(w.xpos);
191
+ const deprelColor = getDeprelColor(w.deprel);
192
+ const nodeStr = `${chalk.dim(w.id + ':')}${wordColor(w.form)} [${chalk.dim(w.xpos)}]`;
193
+ const relStr = ` ──(${deprelColor(w.deprel)})──>`;
194
+ console.log(`${prefix}${isLast ? '└── ' : '├── '}${nodeStr}${relStr}`);
195
+ const children = parentToChildren.get(w.id) || [];
196
+ const childPrefix = prefix + (isLast ? ' ' : '│ ');
197
+ for (let i = 0; i < children.length; i++) {
198
+ printNode(children[i], childPrefix, i === children.length - 1);
199
+ }
200
+ visited.delete(w.id);
201
+ }
202
+ console.log(chalk.bold.yellow('Dependency Tree Visualization:'));
203
+ for (let i = 0; i < roots.length; i++) {
204
+ printNode(roots[i], '', i === roots.length - 1);
205
+ }
206
+ }
207
+ // Wait for a generic keypress
208
+ function waitForKeypress() {
209
+ return new Promise((resolve) => {
210
+ if (process.stdin.isTTY) {
211
+ process.stdin.setRawMode(true);
212
+ }
213
+ process.stdin.resume();
214
+ readline.emitKeypressEvents(process.stdin);
215
+ function onKeypress() {
216
+ process.stdin.removeListener('keypress', onKeypress);
217
+ if (process.stdin.isTTY) {
218
+ process.stdin.setRawMode(false);
219
+ }
220
+ resolve();
221
+ }
222
+ process.stdin.once('keypress', onKeypress);
223
+ });
224
+ }
225
+ // Wait specifically for Enter key
226
+ function waitForEnterKey() {
227
+ return new Promise((resolve) => {
228
+ const startTime = Date.now();
229
+ if (process.stdin.isTTY) {
230
+ process.stdin.setRawMode(true);
231
+ }
232
+ process.stdin.resume();
233
+ readline.emitKeypressEvents(process.stdin);
234
+ function onKeypress(str, key) {
235
+ if (Date.now() - startTime < 100)
236
+ return;
237
+ if (key && key.ctrl && key.name === 'c') {
238
+ process.stdout.write('\u001B[?25h');
239
+ process.exit(0);
240
+ }
241
+ if (key && key.name === 'return') {
242
+ process.stdin.removeListener('keypress', onKeypress);
243
+ if (process.stdin.isTTY) {
244
+ process.stdin.setRawMode(false);
245
+ }
246
+ resolve();
247
+ }
248
+ }
249
+ process.stdin.on('keypress', onKeypress);
250
+ });
251
+ }
252
+ // Enter multi-line paste/input mode (stops on a blank line)
253
+ function readMultilineInput() {
254
+ return new Promise((resolve) => {
255
+ process.stdout.write('\u001B[?25h'); // show cursor
256
+ console.log(chalk.yellow('\nType or paste your text below.'));
257
+ console.log(chalk.dim('When finished, press Enter on an empty line to parse:'));
258
+ console.log(chalk.dim('────────────────────────────────────────────────────────────────────────────────'));
259
+ const lines = [];
260
+ const rl = readline.createInterface({
261
+ input: process.stdin,
262
+ output: process.stdout,
263
+ terminal: false
264
+ });
265
+ rl.on('line', (line) => {
266
+ if (line === '') {
267
+ rl.close();
268
+ }
269
+ else {
270
+ lines.push(line);
271
+ }
272
+ });
273
+ rl.on('close', () => {
274
+ process.stdout.write('\u001B[?25l'); // hide cursor
275
+ resolve(lines.join('\n'));
276
+ });
277
+ });
278
+ }
279
+ // Enter single-line input mode (for file paths, model paths)
280
+ function readSingleLineInput(promptMsg) {
281
+ return new Promise((resolve) => {
282
+ process.stdout.write('\u001B[?25h'); // show cursor
283
+ const rl = readline.createInterface({
284
+ input: process.stdin,
285
+ output: process.stdout
286
+ });
287
+ rl.question(promptMsg, (answer) => {
288
+ rl.close();
289
+ process.stdout.write('\u001B[?25l'); // hide cursor
290
+ resolve(answer.trim());
291
+ });
292
+ });
293
+ }
294
+ // Prompt to return to the main menu with Enter/Escape support
295
+ function promptReturnToMenu() {
296
+ return new Promise((resolve) => {
297
+ const startTime = Date.now();
298
+ console.log(chalk.bold.yellow('\nYou have reached the end of the parses.'));
299
+ process.stdout.write(chalk.cyan('Return to the main menu? (y/n) [Enter=Yes, ESC=No]: '));
300
+ if (process.stdin.isTTY) {
301
+ process.stdin.setRawMode(true);
302
+ }
303
+ process.stdin.resume();
304
+ readline.emitKeypressEvents(process.stdin);
305
+ function onKeypress(str, key) {
306
+ if (Date.now() - startTime < 100)
307
+ return;
308
+ if (key && key.ctrl && key.name === 'c') {
309
+ process.stdout.write('\u001B[?25h');
310
+ process.exit(0);
311
+ }
312
+ const char = (str || '').toLowerCase();
313
+ if (char === 'y' || (key && key.name === 'return')) {
314
+ cleanup();
315
+ resolve(true);
316
+ }
317
+ else if (char === 'n' || (key && key.name === 'escape')) {
318
+ cleanup();
319
+ resolve(false);
320
+ }
321
+ }
322
+ function cleanup() {
323
+ process.stdin.removeListener('keypress', onKeypress);
324
+ if (process.stdin.isTTY) {
325
+ process.stdin.setRawMode(false);
326
+ }
327
+ }
328
+ process.stdin.on('keypress', onKeypress);
329
+ });
330
+ }
331
+ function getVisibleLength(str) {
332
+ return str.replace(/\u001B\[[0-9;]*[a-zA-Z]/g, '').length;
333
+ }
334
+ function padVisible(str, targetLen) {
335
+ const len = getVisibleLength(str);
336
+ if (len >= targetLen)
337
+ return str;
338
+ return str + ' '.repeat(targetLen - len);
339
+ }
340
+ function getLeftUposLines() {
341
+ const lines = [
342
+ chalk.bold.hex('#00DEC8')('▼ Universal POS (UPOS)'),
343
+ chalk.dim('─'.repeat(56))
344
+ ];
345
+ const add = (tag, desc, ex) => {
346
+ lines.push(` ${chalk.bold.cyan(tag.padEnd(8))} ${desc.padEnd(20)} ${chalk.italic.gray(ex)}`);
347
+ };
348
+ add('ADJ', 'Adjective', 'big, red, happy');
349
+ add('ADP', 'Adposition', 'in, on, to');
350
+ add('ADV', 'Adverb', 'quickly, here');
351
+ add('AUX', 'Auxiliary verb', 'has, will, is');
352
+ add('CCONJ', 'Coord. conj.', 'and, but, or');
353
+ add('DET', 'Determiner', 'the, a, some');
354
+ add('INTJ', 'Interjection', 'oh, wow, uh-huh');
355
+ add('NOUN', 'Common noun', 'cat, water, idea');
356
+ add('NUM', 'Numeral', '3, first, II');
357
+ add('PART', 'Particle', "not, 's, to");
358
+ add('PRON', 'Pronoun', 'I, who, this');
359
+ add('PROPN', 'Proper noun', 'London, Bell');
360
+ add('PUNCT', 'Punctuation', ', . ? ! ; :');
361
+ add('SCONJ', 'Subord. conj.', 'because, if');
362
+ add('SYM', 'Symbol', '$, %, +, @');
363
+ add('VERB', 'Verb (main)', 'run, eat, think');
364
+ add('X', 'Other', 'etc., foobar');
365
+ return lines;
366
+ }
367
+ function getRightXposLines() {
368
+ const lines = [
369
+ chalk.bold.hex('#FF9F43')('▼ English Penn Treebank XPOS'),
370
+ chalk.dim('─'.repeat(52))
371
+ ];
372
+ const add = (tag, desc, ex) => {
373
+ lines.push(` ${chalk.bold.cyan(tag.padEnd(6))} ${desc.padEnd(22)} ${chalk.italic.gray(ex)}`);
374
+ };
375
+ add('CC', 'Coord. conjunction', 'and, but, or, yet');
376
+ add('CD', 'Cardinal number', '1, 3.14, 1000');
377
+ add('DT', 'Determiner', 'the, a, an, this');
378
+ add('EX', 'Existential there', 'There is hope');
379
+ add('FW', 'Foreign word', 'vis-à-vis, en route');
380
+ add('IN', 'Prep / subord conj', 'in, because, if');
381
+ add('JJ', 'Adjective', 'big, red');
382
+ add('JJR', 'Adj., comparative', 'bigger, better');
383
+ add('JJS', 'Adj., superlative', 'biggest, best');
384
+ add('LS', 'List item marker', '(1), a., •');
385
+ add('MD', 'Modal', 'can, will, should');
386
+ add('NN', 'Noun, singular', 'cat, water');
387
+ add('NNS', 'Noun, plural', 'cats, ideas');
388
+ add('NNP', 'Proper noun, sing.', 'Bell, Los Angeles');
389
+ add('NNPS', 'Proper noun, plur.', 'Rockies');
390
+ add('PDT', 'Predeterminer', 'all, both, half');
391
+ add('POS', 'Possessive ending', "'s, '");
392
+ add('PRP', 'Personal pronoun', 'I, you, he, them');
393
+ add('PRP$', 'Possessive pronoun', 'my, your, his');
394
+ add('RB', 'Adverb', 'quickly, here');
395
+ add('RBR', 'Adverb, compar.', 'faster, more');
396
+ add('RBS', 'Adverb, superl.', 'fastest, most');
397
+ add('RP', 'Particle', 'up (shut up), off');
398
+ add('SYM', 'Symbol', '$, %, #');
399
+ add('TO', 'to infinitive', 'to go, to eat');
400
+ add('UH', 'Interjection', 'oh, wow');
401
+ add('VB', 'Verb, base form', 'run, eat');
402
+ add('VBD', 'Verb, past tense', 'ran, ate, was');
403
+ add('VBG', 'Verb, gerund/part.', 'running, being');
404
+ add('VBN', 'Verb, past part.', 'eaten, been');
405
+ add('VBP', 'Verb, non-3rd pres', 'I run, they eat');
406
+ add('VBZ', 'Verb, 3rd present', 'he runs, she eats');
407
+ add('WDT', 'Wh-determiner', 'which, that');
408
+ add('WP', 'Wh-pronoun', 'who, what, whom');
409
+ add('WP$', 'Possessive wh-pron', 'whose');
410
+ add('WRB', 'Wh-adverb', 'how, where, why');
411
+ return lines;
412
+ }
413
+ function getPage1Lines() {
414
+ const left = getLeftUposLines();
415
+ const right = getRightXposLines();
416
+ const max = Math.max(left.length, right.length);
417
+ const lines = [];
418
+ for (let i = 0; i < max; i++) {
419
+ const l = left[i] || '';
420
+ const r = right[i] || '';
421
+ lines.push(padVisible(l, 56) + ' │ ' + r);
422
+ }
423
+ return lines;
424
+ }
425
+ function getLeftDeprelLines() {
426
+ const lines = [];
427
+ const addHeader = (text) => {
428
+ if (lines.length > 0)
429
+ lines.push('');
430
+ lines.push(chalk.bold.hex('#00DEC8')(`▼ ${text}`));
431
+ lines.push(chalk.dim('─'.repeat(56)));
432
+ };
433
+ const add = (rel, desc, ex) => {
434
+ lines.push(` ${getDeprelColor(rel)(rel.padEnd(12))} ${desc.padEnd(16)} ${chalk.italic.gray(ex)}`);
435
+ };
436
+ addHeader('Core Arguments (Clausal)');
437
+ add('nsubj', 'Nominal subject', 'Bell makes');
438
+ add('nsubj:pass', 'Passive nom subj', 'Dole was defeated');
439
+ add('obj', 'Direct object', 'reads books');
440
+ add('iobj', 'Indirect object', 'gave him a book');
441
+ addHeader('Non-Core Dependents (Clausal)');
442
+ add('obl', 'Oblique nominal', 'went to Paris');
443
+ add('obl:agent', 'Oblique agent', 'killed by police');
444
+ addHeader('Nominal Dependents');
445
+ add('nmod', 'Nominal modifier', 'the city center');
446
+ add('nmod:poss', 'Possessive mod', 'their offices');
447
+ add('appos', 'Appositional', 'Sam, my brother');
448
+ add('nummod', 'Numeric modifier', '3 sheep');
449
+ addHeader('Clausal Dependents');
450
+ add('csubj', 'Clausal subject', 'What she said makes');
451
+ add('csubj:pass', 'Passive clausal s.', 'That she lied was');
452
+ add('ccomp', 'Clausal comp.', 'says that you like');
453
+ add('xcomp', 'Open clausal comp', 'ready to leave');
454
+ add('advcl', 'Adverbial clause', 'left as night fell');
455
+ add('acl', 'Adnominal clause', 'the man you love');
456
+ add('acl:relcl', 'Relative clause', 'book which you bought');
457
+ return lines;
458
+ }
459
+ function getRightDeprelLines() {
460
+ const lines = [];
461
+ const addHeader = (text) => {
462
+ if (lines.length > 0)
463
+ lines.push('');
464
+ lines.push(chalk.bold.hex('#FF9F43')(`▼ ${text}`));
465
+ lines.push(chalk.dim('─'.repeat(48)));
466
+ };
467
+ const add = (rel, desc, ex) => {
468
+ lines.push(` ${getDeprelColor(rel)(rel.padEnd(12))} ${desc.padEnd(16)} ${chalk.italic.gray(ex)}`);
469
+ };
470
+ addHeader('Modifiers');
471
+ add('advmod', 'Adverbial mod.', 'genetically modified');
472
+ add('neg', 'Negation modifier', 'not a scientist');
473
+ add('amod', 'Adjectival mod.', 'red meat');
474
+ add('det', 'Determiner', 'the man');
475
+ addHeader('Function Words');
476
+ add('aux', 'Auxiliary verb', 'has died');
477
+ add('aux:pass', 'Passive auxiliary', 'was killed');
478
+ add('cop', 'Copula', 'is big');
479
+ add('mark', 'Marker (subord.)', 'left because she');
480
+ add('case', 'Case-marking', 'in the room');
481
+ addHeader('Coordination');
482
+ add('conj', 'Conjunct', 'big and honest');
483
+ add('cc', 'Coordinating conj.', 'big and honest');
484
+ add('cc:preconj', 'Preconjunct', 'both boys and girls');
485
+ addHeader('Compounds & MWE');
486
+ add('compound', 'Compound', 'ice cream, shut down');
487
+ add('compound:prt', 'Phrasal verb part', 'shut down, take off');
488
+ add('flat', 'Flat MWE', 'New York, ad hoc');
489
+ add('flat:name', 'Name (flat struct)', 'Marie Catherine');
490
+ add('fixed', 'Fixed MWE', 'because of');
491
+ addHeader('Root & Other');
492
+ add('root', 'Root of sentence', 'ROOT → love');
493
+ add('dep', 'Unspecified dep.', 'fallback case');
494
+ add('discourse', 'Discourse element', 'well, like, actually');
495
+ add('expl', 'Expletive', 'There is a ghost');
496
+ add('goeswith', 'Goes with', 'without');
497
+ return lines;
498
+ }
499
+ function getPage2Lines() {
500
+ const left = getLeftDeprelLines();
501
+ const right = getRightDeprelLines();
502
+ const max = Math.max(left.length, right.length);
503
+ const lines = [];
504
+ for (let i = 0; i < max; i++) {
505
+ const l = left[i] || '';
506
+ const r = right[i] || '';
507
+ lines.push(padVisible(l, 56) + ' │ ' + r);
508
+ }
509
+ return lines;
510
+ }
511
+ function getPage3Lines() {
512
+ const lines = [];
513
+ const addHeader = (text) => {
514
+ if (lines.length > 0)
515
+ lines.push('');
516
+ lines.push(chalk.bold.hex('#FF9F43')(text));
517
+ lines.push(chalk.dim('─'.repeat(80)));
518
+ };
519
+ const add = (legacy, ud, notes) => {
520
+ lines.push(` ${chalk.red(legacy.padEnd(10))} ──> ${chalk.green(ud.padEnd(12))} ${chalk.italic.gray(notes)}`);
521
+ };
522
+ addHeader('Legacy Stanford Relations (for reference when reading old data)');
523
+ add('dobj', 'obj', 'Direct object merged into obj');
524
+ add('pobj', 'obl', 'Object of preposition → oblique');
525
+ add('nn', 'compound', 'Noun compound modifier');
526
+ add('mwe', 'fixed', 'Multi-word expression');
527
+ add('rcmod', 'acl:relcl', 'Relative clause modifier');
528
+ add('prepc', 'advcl+mark', 'Prepositional clausal modifier');
529
+ add('agent', 'obl:agent', 'Passive agent (collapsed)');
530
+ add('xsubj', 'N/A', 'Controlling subject (enhanced only)');
531
+ addHeader('Key Mappings & Notes');
532
+ lines.push(chalk.white(' • UPOS VERB/AUX split distinguishes main vs. auxiliary verbs.'));
533
+ lines.push(chalk.white(' • Penn IN covers both prepositions and subordinating conjunctions;'));
534
+ lines.push(chalk.white(' UD distinguishes ADP/SCONJ at UPOS level and case/mark at deprel level.'));
535
+ lines.push(chalk.white(" • 'TO' in Penn is PART in UPOS."));
536
+ return lines;
537
+ }
538
+ // Loop for paginated parser outputs with arrow key traversal, legend toggle, and Escape exit
539
+ function viewParses(sentences, isVerse, startIndex = 0) {
540
+ return new Promise((resolve) => {
541
+ let index = startIndex;
542
+ let showLegend = false;
543
+ let legendPage = 0;
544
+ const startTime = Date.now();
545
+ function draw() {
546
+ if (showLegend) {
547
+ console.clear();
548
+ console.log(chalk.bold.hex('#FF9F43')(`=== Grammatical Reference: Page ${legendPage + 1} of 3 ===`));
549
+ console.log(chalk.cyan('Controls: [←/→] Switch Page | [↑/ESC/q] Return to Parses\n'));
550
+ let lines = [];
551
+ if (legendPage === 0) {
552
+ lines = getPage1Lines();
553
+ }
554
+ else if (legendPage === 1) {
555
+ lines = getPage2Lines();
556
+ }
557
+ else {
558
+ lines = getPage3Lines();
559
+ }
560
+ for (const line of lines) {
561
+ console.log(line);
562
+ }
563
+ console.log('\n' + chalk.dim('─'.repeat(100)));
564
+ return;
565
+ }
566
+ console.clear();
567
+ const unitName = isVerse ? 'Line' : 'Sentence';
568
+ console.log(chalk.bold.hex('#FF9F43')(`=== ${unitName} ${index + 1} of ${sentences.length} ===`));
569
+ const s = sentences[index];
570
+ if (!s) {
571
+ console.log(chalk.red('Error: Sentence index out of bounds.'));
572
+ cleanup();
573
+ resolve();
574
+ return;
575
+ }
576
+ console.log(chalk.bold('\nText: ') + chalk.white(s.text));
577
+ printWordTable(s.words);
578
+ console.log(chalk.dim('\n' + '─'.repeat(70)));
579
+ printGraphEdges(s.words);
580
+ console.log(chalk.dim('\n' + '─'.repeat(70)));
581
+ renderDependencyTree(s.words);
582
+ console.log(chalk.dim('\n' + '─'.repeat(70)));
583
+ console.log(chalk.cyan('Controls: [→/Enter] Next | [←] Prev | [↓] View Reference Legend | [ESC/q] Exit to Menu'));
584
+ }
585
+ draw();
586
+ if (process.stdin.isTTY) {
587
+ process.stdin.setRawMode(true);
588
+ }
589
+ process.stdin.resume();
590
+ readline.emitKeypressEvents(process.stdin);
591
+ function onKeypress(str, key) {
592
+ if (Date.now() - startTime < 100)
593
+ return;
594
+ if (key && key.ctrl && key.name === 'c') {
595
+ process.stdout.write('\u001B[?25h'); // show cursor
596
+ process.exit(0);
597
+ }
598
+ if (showLegend) {
599
+ if (key && key.name === 'up') {
600
+ showLegend = false;
601
+ draw();
602
+ }
603
+ else if (key && key.name === 'left') {
604
+ if (legendPage > 0) {
605
+ legendPage--;
606
+ draw();
607
+ }
608
+ }
609
+ else if ((key && key.name === 'right') || (key && key.name === 'return')) {
610
+ if (legendPage < 2) {
611
+ legendPage++;
612
+ draw();
613
+ }
614
+ }
615
+ else if ((key && key.name === 'escape') || str === 'q' || str === 'Q') {
616
+ showLegend = false;
617
+ draw();
618
+ }
619
+ return;
620
+ }
621
+ // Escape or 'q' key -> return to main menu
622
+ if ((key && key.name === 'escape') || str === 'q' || str === 'Q') {
623
+ cleanup();
624
+ resolve();
625
+ return;
626
+ }
627
+ if (key && key.name === 'down') {
628
+ showLegend = true;
629
+ legendPage = 0;
630
+ draw();
631
+ }
632
+ else if (key && key.name === 'left') {
633
+ if (index > 0) {
634
+ index--;
635
+ draw();
636
+ }
637
+ }
638
+ else if ((key && key.name === 'right') || (key && key.name === 'return')) {
639
+ if (index < sentences.length - 1) {
640
+ index++;
641
+ draw();
642
+ }
643
+ else {
644
+ // We are past the last parse! Prompt the user
645
+ cleanup();
646
+ promptReturnToMenu().then((shouldReturn) => {
647
+ if (shouldReturn) {
648
+ resolve();
649
+ }
650
+ else {
651
+ // Stay on the last parse screen by restarting the loop at the last index
652
+ viewParses(sentences, isVerse, sentences.length - 1).then(resolve);
653
+ }
654
+ });
655
+ }
656
+ }
657
+ }
658
+ function onResize() {
659
+ draw();
660
+ }
661
+ function cleanup() {
662
+ process.stdin.removeListener('keypress', onKeypress);
663
+ process.stdout.removeListener('resize', onResize);
664
+ if (process.stdin.isTTY) {
665
+ process.stdin.setRawMode(false);
666
+ }
667
+ }
668
+ process.stdin.on('keypress', onKeypress);
669
+ process.stdout.on('resize', onResize);
670
+ });
671
+ }
672
+ // Render menu selection
673
+ function selectMenuOption(items) {
674
+ return new Promise((resolve) => {
675
+ let selected = 0;
676
+ function draw() {
677
+ console.clear();
678
+ console.log(chalk.bold.hex('#FF9F43')('=== UDPipe 1.4.0 Interactive Parser CLI ==='));
679
+ console.log(chalk.bold.cyan(`Active Model: `) + chalk.white(currentModelName));
680
+ console.log(chalk.dim(`Model Path: ${currentModelPath}\n`));
681
+ console.log(chalk.dim('Use Up/Down Arrow keys to navigate, press Enter to select.\n'));
682
+ for (let i = 0; i < items.length; i++) {
683
+ if (i === selected) {
684
+ console.log(chalk.bold.cyan(` › ${items[i]}`));
685
+ }
686
+ else {
687
+ console.log(chalk.dim(` ${items[i]}`));
688
+ }
689
+ }
690
+ }
691
+ draw();
692
+ if (process.stdin.isTTY) {
693
+ process.stdin.setRawMode(true);
694
+ }
695
+ process.stdin.resume();
696
+ readline.emitKeypressEvents(process.stdin);
697
+ function onKeypress(str, key) {
698
+ if (key && key.ctrl && key.name === 'c') {
699
+ process.stdout.write('\u001B[?25h'); // show cursor
700
+ process.exit(0);
701
+ }
702
+ if (key && key.name === 'up') {
703
+ selected = (selected - 1 + items.length) % items.length;
704
+ draw();
705
+ }
706
+ else if (key && key.name === 'down') {
707
+ selected = (selected + 1) % items.length;
708
+ draw();
709
+ }
710
+ else if (key && key.name === 'return') {
711
+ cleanup();
712
+ resolve(selected);
713
+ }
714
+ }
715
+ function onResize() {
716
+ draw();
717
+ }
718
+ function cleanup() {
719
+ process.stdin.removeListener('keypress', onKeypress);
720
+ process.stdout.removeListener('resize', onResize);
721
+ if (process.stdin.isTTY) {
722
+ process.stdin.setRawMode(false);
723
+ }
724
+ }
725
+ process.stdin.on('keypress', onKeypress);
726
+ process.stdout.on('resize', onResize);
727
+ });
728
+ }
729
+ // Display POS tag and dependency relation reference
730
+ function showInformation() {
731
+ return new Promise((resolve) => {
732
+ let legendPage = 0;
733
+ const startTime = Date.now();
734
+ function draw() {
735
+ console.clear();
736
+ console.log(chalk.bold.hex('#FF9F43')(`=== Grammatical Reference: Page ${legendPage + 1} of 3 ===`));
737
+ console.log(chalk.cyan('Controls: [←/→] Switch Page | [ESC/q/Enter] Return to Main Menu\n'));
738
+ let lines = [];
739
+ if (legendPage === 0) {
740
+ lines = getPage1Lines();
741
+ }
742
+ else if (legendPage === 1) {
743
+ lines = getPage2Lines();
744
+ }
745
+ else {
746
+ lines = getPage3Lines();
747
+ }
748
+ for (const line of lines) {
749
+ console.log(line);
750
+ }
751
+ console.log('\n' + chalk.dim('─'.repeat(100)));
752
+ }
753
+ draw();
754
+ if (process.stdin.isTTY) {
755
+ process.stdin.setRawMode(true);
756
+ }
757
+ process.stdin.resume();
758
+ readline.emitKeypressEvents(process.stdin);
759
+ function onKeypress(str, key) {
760
+ if (Date.now() - startTime < 100)
761
+ return;
762
+ if (key && key.ctrl && key.name === 'c') {
763
+ process.stdout.write('\u001B[?25h');
764
+ process.exit(0);
765
+ }
766
+ if ((key && key.name === 'escape') || str === 'q' || str === 'Q') {
767
+ cleanup();
768
+ resolve();
769
+ return;
770
+ }
771
+ if (key && key.name === 'left') {
772
+ if (legendPage > 0) {
773
+ legendPage--;
774
+ draw();
775
+ }
776
+ }
777
+ else if (key && key.name === 'right') {
778
+ if (legendPage < 2) {
779
+ legendPage++;
780
+ draw();
781
+ }
782
+ }
783
+ else if (key && key.name === 'return') {
784
+ if (legendPage < 2) {
785
+ legendPage++;
786
+ draw();
787
+ }
788
+ else {
789
+ cleanup();
790
+ resolve();
791
+ }
792
+ }
793
+ }
794
+ function onResize() {
795
+ draw();
796
+ }
797
+ function cleanup() {
798
+ process.stdin.removeListener('keypress', onKeypress);
799
+ process.stdout.removeListener('resize', onResize);
800
+ if (process.stdin.isTTY) {
801
+ process.stdin.setRawMode(false);
802
+ }
803
+ }
804
+ process.stdin.on('keypress', onKeypress);
805
+ process.stdout.on('resize', onResize);
806
+ });
807
+ }
808
+ const menuItems = [
809
+ 'Verse Mode (Parse pasted text line-by-line)',
810
+ 'Prose Mode (Parse pasted text with sentence segmenter)',
811
+ 'Parse from File (Verse Mode)',
812
+ 'Parse from File (Prose Mode)',
813
+ 'Load Model File (.udpipe model path)',
814
+ 'Information (Reference guide for tags)',
815
+ 'Exit'
816
+ ];
817
+ async function mainLoop() {
818
+ process.stdout.write('\u001B[?25l'); // hide cursor
819
+ while (true) {
820
+ const choice = await selectMenuOption(menuItems);
821
+ if (choice === 0 || choice === 1) {
822
+ // Verse (0) or Prose (1) mode
823
+ const isVerse = (choice === 0);
824
+ const text = await readMultilineInput();
825
+ if (text.trim() !== '') {
826
+ console.log(chalk.bold.greenBright('\nPress ENTER to continue...'));
827
+ await waitForEnterKey();
828
+ const sentences = isVerse ? nlp.parseLines(text) : nlp.parse(text);
829
+ await viewParses(sentences, isVerse);
830
+ }
831
+ }
832
+ else if (choice === 2 || choice === 3) {
833
+ // Parse from File (Verse (2) or Prose (3))
834
+ const isVerse = (choice === 2);
835
+ const filepath = await readSingleLineInput('\nEnter the path to the text file to parse: ');
836
+ if (filepath && existsSync(filepath)) {
837
+ try {
838
+ const text = readFileSync(filepath, 'utf-8');
839
+ console.clear();
840
+ console.log(chalk.bold.hex('#FF9F43')(`=== File Content Preview (${basename(filepath)}) ===\n`));
841
+ const fileLines = text.split('\n');
842
+ const preview = fileLines.slice(0, 20).join('\n');
843
+ console.log(chalk.white(preview));
844
+ if (fileLines.length > 20) {
845
+ console.log(chalk.dim(`\n... and ${fileLines.length - 20} more lines`));
846
+ }
847
+ console.log(chalk.dim('\n' + '─'.repeat(70)));
848
+ console.log(chalk.bold.greenBright('Press ENTER to continue...'));
849
+ await waitForEnterKey();
850
+ const sentences = isVerse ? nlp.parseLines(text) : nlp.parse(text);
851
+ await viewParses(sentences, isVerse);
852
+ }
853
+ catch (err) {
854
+ console.log(chalk.red(`\nError reading file: ${err.message}`));
855
+ console.log(chalk.dim('Press any key to return to the main menu...'));
856
+ await waitForKeypress();
857
+ }
858
+ }
859
+ else {
860
+ console.log(chalk.red(`\nFile not found at: ${filepath}`));
861
+ console.log(chalk.dim('Press any key to return to the main menu...'));
862
+ await waitForKeypress();
863
+ }
864
+ }
865
+ else if (choice === 4) {
866
+ // Load Model File
867
+ const modelpath = await readSingleLineInput('\nEnter the path to your .udpipe model file: ');
868
+ if (modelpath && existsSync(modelpath)) {
869
+ try {
870
+ console.log(chalk.yellow('\nLoading new model...'));
871
+ const newEngine = new WasmEngine({ modelPath: modelpath });
872
+ nlp = new UDPipe({ engine: newEngine });
873
+ currentModelPath = modelpath;
874
+ currentModelName = basename(modelpath);
875
+ console.log(chalk.green('✔ Model loaded successfully!'));
876
+ }
877
+ catch (err) {
878
+ console.log(chalk.red(`\nFailed to load model: ${err.message}`));
879
+ }
880
+ }
881
+ else {
882
+ console.log(chalk.red(`\nModel file not found at: ${modelpath}`));
883
+ }
884
+ console.log(chalk.dim('Press any key to return to the main menu...'));
885
+ await waitForKeypress();
886
+ }
887
+ else if (choice === 5) {
888
+ // Information Reference
889
+ await showInformation();
890
+ }
891
+ else if (choice === 6) {
892
+ // Exit
893
+ console.clear();
894
+ process.stdout.write('\u001B[?25h'); // show cursor
895
+ console.log(chalk.green('Goodbye!\n'));
896
+ process.exit(0);
897
+ }
898
+ }
899
+ }
900
+ // Start the application
901
+ mainLoop().catch((err) => {
902
+ process.stdout.write('\u001B[?25h'); // show cursor
903
+ console.error(chalk.red('Fatal error in CLI loop:'), err);
904
+ process.exit(1);
905
+ });
906
+ //# sourceMappingURL=cli.js.map