glost 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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/example.d.ts +10 -0
- package/dist/example.d.ts.map +1 -0
- package/dist/example.js +82 -0
- package/dist/example.js.map +1 -0
- package/dist/guards.d.ts +103 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +264 -0
- package/dist/guards.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/mock-data.d.ts +35 -0
- package/dist/mock-data.d.ts.map +1 -0
- package/dist/mock-data.js +494 -0
- package/dist/mock-data.js.map +1 -0
- package/dist/nodes.d.ts +68 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +181 -0
- package/dist/nodes.js.map +1 -0
- package/dist/types.d.ts +379 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +203 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +497 -0
- package/dist/utils.js.map +1 -0
- package/dist/validators.d.ts +1876 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +302 -0
- package/dist/validators.js.map +1 -0
- package/package.json +67 -0
- package/src/example.ts +186 -0
- package/src/guards.ts +341 -0
- package/src/index.ts +69 -0
- package/src/mock-data.ts +635 -0
- package/src/nodes.ts +301 -0
- package/src/types.ts +565 -0
- package/src/utils.ts +653 -0
- package/src/validators.ts +336 -0
- package/tsconfig.json +9 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import { is as isNode } from "unist-util-is";
|
|
2
|
+
import { visit } from "unist-util-visit";
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
LanguageCode,
|
|
6
|
+
GLOSTCharacter,
|
|
7
|
+
GLOSTClause,
|
|
8
|
+
GLOSTNode,
|
|
9
|
+
GLOSTParagraph,
|
|
10
|
+
GLOSTPhrase,
|
|
11
|
+
GLOSTRoot,
|
|
12
|
+
GLOSTSentence,
|
|
13
|
+
GLOSTSyllable,
|
|
14
|
+
GLOSTWord,
|
|
15
|
+
TranscriptionSystem,
|
|
16
|
+
TransliterationData,
|
|
17
|
+
} from "./types";
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// BCP-47 Language Tag Utilities
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse a BCP-47 language tag into its components
|
|
25
|
+
* Format: language[-script][-region][-variant]
|
|
26
|
+
*/
|
|
27
|
+
export function parseLanguageTag(tag: string): {
|
|
28
|
+
language: string;
|
|
29
|
+
script?: string;
|
|
30
|
+
region?: string;
|
|
31
|
+
variant?: string;
|
|
32
|
+
fullTag: string;
|
|
33
|
+
} {
|
|
34
|
+
const parts = tag.split("-");
|
|
35
|
+
const result = {
|
|
36
|
+
language: parts[0] || "",
|
|
37
|
+
script: undefined as string | undefined,
|
|
38
|
+
region: undefined as string | undefined,
|
|
39
|
+
variant: undefined as string | undefined,
|
|
40
|
+
fullTag: tag,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (parts.length >= 2) {
|
|
44
|
+
// Check if second part is a script (4 letters) or region (2-3 letters)
|
|
45
|
+
if (parts[1] && parts[1].length === 4 && /^[A-Za-z]{4}$/.test(parts[1])) {
|
|
46
|
+
result.script = parts[1];
|
|
47
|
+
if (parts.length >= 3 && parts[2]) {
|
|
48
|
+
result.region = parts[2];
|
|
49
|
+
if (parts.length >= 4) {
|
|
50
|
+
result.variant = parts.slice(3).join("-");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
// Second part is likely a region
|
|
55
|
+
if (parts[1]) {
|
|
56
|
+
result.region = parts[1];
|
|
57
|
+
if (parts.length >= 3) {
|
|
58
|
+
result.variant = parts.slice(2).join("-");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the base language from a BCP-47 tag
|
|
69
|
+
* Examples: "en-US" -> "en", "zh-CN" -> "zh"
|
|
70
|
+
*/
|
|
71
|
+
export function getBaseLanguage(tag: string): string {
|
|
72
|
+
const parts = tag.split("-");
|
|
73
|
+
return parts[0] || tag;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if two language tags are compatible (same base language)
|
|
78
|
+
* Examples: "en-US" and "en-GB" are compatible
|
|
79
|
+
*/
|
|
80
|
+
export function areLanguagesCompatible(tag1: string, tag2: string): boolean {
|
|
81
|
+
return getBaseLanguage(tag1) === getBaseLanguage(tag2);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Find the best matching language tag from available options
|
|
86
|
+
* Prioritizes exact matches, then region matches, then base language matches
|
|
87
|
+
*/
|
|
88
|
+
export function findBestLanguageMatch(
|
|
89
|
+
target: string,
|
|
90
|
+
available: string[],
|
|
91
|
+
): string | null {
|
|
92
|
+
if (available.includes(target)) {
|
|
93
|
+
return target;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const targetParts = parseLanguageTag(target);
|
|
97
|
+
|
|
98
|
+
// Try to find region-specific matches
|
|
99
|
+
for (const option of available) {
|
|
100
|
+
const optionParts = parseLanguageTag(option);
|
|
101
|
+
if (
|
|
102
|
+
optionParts.language === targetParts.language &&
|
|
103
|
+
optionParts.region === targetParts.region
|
|
104
|
+
) {
|
|
105
|
+
return option;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Try to find base language matches
|
|
110
|
+
for (const option of available) {
|
|
111
|
+
const optionParts = parseLanguageTag(option);
|
|
112
|
+
if (optionParts.language === targetParts.language) {
|
|
113
|
+
return option;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get a fallback language tag when the exact one isn't available
|
|
122
|
+
* Examples: "en-US" -> "en", "zh-CN" -> "zh"
|
|
123
|
+
*/
|
|
124
|
+
export function getLanguageFallback(tag: string): string {
|
|
125
|
+
const parts = parseLanguageTag(tag);
|
|
126
|
+
return parts.language;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Normalize a language tag to standard format
|
|
131
|
+
* Converts to lowercase and ensures proper formatting
|
|
132
|
+
*/
|
|
133
|
+
export function normalizeLanguageTag(tag: string): string {
|
|
134
|
+
const parts = tag.split("-");
|
|
135
|
+
const language = parts[0]?.toLowerCase() || "";
|
|
136
|
+
|
|
137
|
+
if (parts.length === 1) {
|
|
138
|
+
return language;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Handle script (4 letters, title case)
|
|
142
|
+
if (parts[1] && parts[1].length === 4) {
|
|
143
|
+
const script = parts[1]
|
|
144
|
+
.toLowerCase()
|
|
145
|
+
.replace(/\b\w/g, (l) => l.toUpperCase());
|
|
146
|
+
|
|
147
|
+
if (parts.length === 2) {
|
|
148
|
+
return `${language}-${script}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Handle region (2-3 letters, uppercase)
|
|
152
|
+
const region = parts[2]?.toUpperCase() || "";
|
|
153
|
+
|
|
154
|
+
if (parts.length === 3) {
|
|
155
|
+
return `${language}-${script}-${region}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle variants (lowercase)
|
|
159
|
+
const variants = parts.slice(3).join("-").toLowerCase();
|
|
160
|
+
return `${language}-${script}-${region}-${variants}`;
|
|
161
|
+
} else {
|
|
162
|
+
// No script, just language-region
|
|
163
|
+
const region = parts[1]?.toUpperCase() || "";
|
|
164
|
+
|
|
165
|
+
if (parts.length === 2) {
|
|
166
|
+
return `${language}-${region}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Handle variants
|
|
170
|
+
const variants = parts.slice(2).join("-").toLowerCase();
|
|
171
|
+
return `${language}-${region}-${variants}`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Check if a language tag is valid BCP-47 format
|
|
177
|
+
*/
|
|
178
|
+
export function isValidLanguageTag(tag: string): boolean {
|
|
179
|
+
// Basic BCP-47 validation
|
|
180
|
+
const bcp47Pattern =
|
|
181
|
+
/^[a-z]{2,3}(-[A-Za-z]{4})?(-[A-Za-z]{2,3})?(-[A-Za-z0-9]+)*$/;
|
|
182
|
+
return bcp47Pattern.test(tag);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Enhanced Tree Traversal Utilities (using unist-util-visit)
|
|
187
|
+
// ============================================================================
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get all word nodes from an GLOST tree
|
|
191
|
+
*/
|
|
192
|
+
export function getAllWords(node: GLOSTNode): GLOSTWord[] {
|
|
193
|
+
const words: GLOSTWord[] = [];
|
|
194
|
+
|
|
195
|
+
visit(node, "WordNode", (wordNode) => {
|
|
196
|
+
if (isGLOSTWord(wordNode)) {
|
|
197
|
+
words.push(wordNode);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return words;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get all sentence nodes from an GLOST tree
|
|
206
|
+
*/
|
|
207
|
+
export function getAllSentences(node: GLOSTNode): GLOSTSentence[] {
|
|
208
|
+
const sentences: GLOSTSentence[] = [];
|
|
209
|
+
|
|
210
|
+
visit(node, "SentenceNode", (sentenceNode) => {
|
|
211
|
+
if (isGLOSTSentence(sentenceNode)) {
|
|
212
|
+
sentences.push(sentenceNode);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return sentences;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get all paragraph nodes from an GLOST tree
|
|
221
|
+
*/
|
|
222
|
+
export function getAllParagraphs(node: GLOSTNode): GLOSTParagraph[] {
|
|
223
|
+
const paragraphs: GLOSTParagraph[] = [];
|
|
224
|
+
|
|
225
|
+
visit(node, "ParagraphNode", (paragraphNode) => {
|
|
226
|
+
if (isGLOSTParagraph(paragraphNode)) {
|
|
227
|
+
paragraphs.push(paragraphNode);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return paragraphs;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get all clause nodes from an GLOST tree
|
|
236
|
+
*/
|
|
237
|
+
export function getAllClauses(node: GLOSTNode): GLOSTClause[] {
|
|
238
|
+
const clauses: GLOSTClause[] = [];
|
|
239
|
+
|
|
240
|
+
visit(node, "ClauseNode", (clauseNode) => {
|
|
241
|
+
if (isGLOSTClause(clauseNode)) {
|
|
242
|
+
clauses.push(clauseNode);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return clauses;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get all phrase nodes from an GLOST tree
|
|
251
|
+
*/
|
|
252
|
+
export function getAllPhrases(node: GLOSTNode): GLOSTPhrase[] {
|
|
253
|
+
const phrases: GLOSTPhrase[] = [];
|
|
254
|
+
|
|
255
|
+
visit(node, "PhraseNode", (phraseNode) => {
|
|
256
|
+
if (isGLOSTPhrase(phraseNode)) {
|
|
257
|
+
phrases.push(phraseNode);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return phrases;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get all syllable nodes from an GLOST tree
|
|
266
|
+
*/
|
|
267
|
+
export function getAllSyllables(node: GLOSTNode): GLOSTSyllable[] {
|
|
268
|
+
const syllables: GLOSTSyllable[] = [];
|
|
269
|
+
|
|
270
|
+
visit(node, "SyllableNode", (syllableNode) => {
|
|
271
|
+
if (isGLOSTSyllable(syllableNode)) {
|
|
272
|
+
syllables.push(syllableNode);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return syllables;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get all character nodes from an GLOST tree
|
|
281
|
+
*/
|
|
282
|
+
export function getAllCharacters(node: GLOSTNode): GLOSTCharacter[] {
|
|
283
|
+
const characters: GLOSTCharacter[] = [];
|
|
284
|
+
|
|
285
|
+
visit(node, "CharacterNode", (characterNode) => {
|
|
286
|
+
if (isGLOSTCharacter(characterNode)) {
|
|
287
|
+
characters.push(characterNode);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return characters;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Find nodes by type with better typing
|
|
296
|
+
*/
|
|
297
|
+
export function findNodesByType<T extends GLOSTNode>(
|
|
298
|
+
node: GLOSTNode,
|
|
299
|
+
type: string,
|
|
300
|
+
): T[] {
|
|
301
|
+
const results: T[] = [];
|
|
302
|
+
|
|
303
|
+
visit(node, type, (foundNode) => {
|
|
304
|
+
results.push(foundNode as T);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return results;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ============================================================================
|
|
311
|
+
// New Utilities for Transcription Components
|
|
312
|
+
// ============================================================================
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get all words from a document with proper typing
|
|
316
|
+
*/
|
|
317
|
+
export function getWordsFromDocument(doc: GLOSTRoot): GLOSTWord[] {
|
|
318
|
+
return getAllWords(doc);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get the first sentence from a document
|
|
323
|
+
*/
|
|
324
|
+
export function getFirstSentence(doc: GLOSTRoot): GLOSTSentence | null {
|
|
325
|
+
const paragraphs = getAllParagraphs(doc);
|
|
326
|
+
if (paragraphs.length === 0) return null;
|
|
327
|
+
|
|
328
|
+
const firstParagraph = paragraphs[0];
|
|
329
|
+
if (!firstParagraph) return null;
|
|
330
|
+
|
|
331
|
+
const sentences = getAllSentences(firstParagraph);
|
|
332
|
+
if (sentences.length === 0) return null;
|
|
333
|
+
|
|
334
|
+
const firstSentence = sentences[0];
|
|
335
|
+
return firstSentence || null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get words from a specific sentence
|
|
340
|
+
*/
|
|
341
|
+
export function getWordsFromSentence(sentence: GLOSTSentence): GLOSTWord[] {
|
|
342
|
+
return getAllWords(sentence);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get words from a specific paragraph
|
|
347
|
+
*/
|
|
348
|
+
export function getWordsFromParagraph(paragraph: GLOSTParagraph): GLOSTWord[] {
|
|
349
|
+
const words: GLOSTWord[] = [];
|
|
350
|
+
|
|
351
|
+
visit(paragraph, "WordNode", (wordNode) => {
|
|
352
|
+
if (isGLOSTWord(wordNode)) {
|
|
353
|
+
words.push(wordNode);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return words;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Find word nodes with specific language
|
|
362
|
+
*/
|
|
363
|
+
export function findWordsByLanguage(
|
|
364
|
+
node: GLOSTNode,
|
|
365
|
+
lang: LanguageCode,
|
|
366
|
+
): GLOSTWord[] {
|
|
367
|
+
const words = getAllWords(node);
|
|
368
|
+
return words.filter((word) => word.lang === lang);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Find word nodes with specific transcription system
|
|
373
|
+
*/
|
|
374
|
+
export function findWordsByTranscriptionSystem(
|
|
375
|
+
node: GLOSTNode,
|
|
376
|
+
system: TranscriptionSystem,
|
|
377
|
+
): GLOSTWord[] {
|
|
378
|
+
const words = getAllWords(node);
|
|
379
|
+
return words.filter(
|
|
380
|
+
(word) => word.transcription && word.transcription[system],
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ============================================================================
|
|
385
|
+
// Enhanced Type Guards (using unist-util-is)
|
|
386
|
+
// ============================================================================
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Enhanced type guards for the new GLOST types
|
|
390
|
+
*/
|
|
391
|
+
export function isGLOSTWord(node: any): node is GLOSTWord {
|
|
392
|
+
return (
|
|
393
|
+
isNode(node, "WordNode") && "transcription" in node && "metadata" in node
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function isGLOSTSentence(node: any): node is GLOSTSentence {
|
|
398
|
+
return (
|
|
399
|
+
isNode(node, "SentenceNode") &&
|
|
400
|
+
"lang" in node &&
|
|
401
|
+
"script" in node &&
|
|
402
|
+
"originalText" in node
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function isGLOSTParagraph(node: any): node is GLOSTParagraph {
|
|
407
|
+
return isNode(node, "ParagraphNode");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function isGLOSTRoot(node: any): node is GLOSTRoot {
|
|
411
|
+
return isNode(node, "Root") && "lang" in node && "script" in node;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Type guard for GLOSTClause nodes
|
|
416
|
+
*/
|
|
417
|
+
export function isGLOSTClause(node: any): node is GLOSTClause {
|
|
418
|
+
return isNode(node, "ClauseNode") && "clauseType" in node;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Type guard for GLOSTPhrase nodes
|
|
423
|
+
*/
|
|
424
|
+
export function isGLOSTPhrase(node: any): node is GLOSTPhrase {
|
|
425
|
+
return isNode(node, "PhraseNode") && "phraseType" in node;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Type guard for GLOSTSyllable nodes
|
|
430
|
+
*/
|
|
431
|
+
export function isGLOSTSyllable(node: any): node is GLOSTSyllable {
|
|
432
|
+
return isNode(node, "SyllableNode") && "structure" in node;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Type guard for GLOSTCharacter nodes
|
|
437
|
+
*/
|
|
438
|
+
export function isGLOSTCharacter(node: any): node is GLOSTCharacter {
|
|
439
|
+
return isNode(node, "CharacterNode") && "value" in node;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ============================================================================
|
|
443
|
+
// Utility Functions for Transcription Components
|
|
444
|
+
// ============================================================================
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Extract text value from a word node
|
|
448
|
+
*/
|
|
449
|
+
export function getWordText(word: GLOSTWord): string {
|
|
450
|
+
const textNode = word.children.find((child) => child.type === "TextNode");
|
|
451
|
+
return textNode?.value ?? "";
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Get transcription for a specific system
|
|
456
|
+
*/
|
|
457
|
+
export function getWordTranscription(
|
|
458
|
+
word: GLOSTWord,
|
|
459
|
+
system: TranscriptionSystem,
|
|
460
|
+
): string | null {
|
|
461
|
+
return word.transcription[system]?.text ?? null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Check if a word has transcription for a specific system
|
|
466
|
+
*/
|
|
467
|
+
export function hasWordTranscription(
|
|
468
|
+
word: GLOSTWord,
|
|
469
|
+
system: TranscriptionSystem,
|
|
470
|
+
): boolean {
|
|
471
|
+
return system in word.transcription && !!word.transcription[system]?.text;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Get word translation for a specific language
|
|
476
|
+
* @param word - The word node
|
|
477
|
+
* @param language - Target language code (default: "en-US")
|
|
478
|
+
* @returns Translation string or empty string if not found
|
|
479
|
+
*/
|
|
480
|
+
export function getWordTranslation(
|
|
481
|
+
word: GLOSTWord,
|
|
482
|
+
language = "en-US",
|
|
483
|
+
): string {
|
|
484
|
+
// Check extras.translations first (preferred format)
|
|
485
|
+
if (word.extras?.translations?.[language]) {
|
|
486
|
+
return word.extras.translations[language];
|
|
487
|
+
}
|
|
488
|
+
// Also check short language code (e.g., "en" for "en-US")
|
|
489
|
+
const shortLang = language.split("-")[0];
|
|
490
|
+
if (shortLang && word.extras?.translations?.[shortLang]) {
|
|
491
|
+
return word.extras.translations[shortLang];
|
|
492
|
+
}
|
|
493
|
+
return "";
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Get word meaning/definition
|
|
498
|
+
* @deprecated Use getWordTranslation for multi-language support.
|
|
499
|
+
* This function is kept for backward compatibility.
|
|
500
|
+
*/
|
|
501
|
+
export function getWordMeaning(word: GLOSTWord): string {
|
|
502
|
+
// Priority: extras.translations (preferred) > metadata.meaning (deprecated) > shortDefinition (deprecated)
|
|
503
|
+
const translation = getWordTranslation(word, "en-US");
|
|
504
|
+
if (translation) return translation;
|
|
505
|
+
|
|
506
|
+
return (
|
|
507
|
+
word.metadata?.meaning ?? word.shortDefinition ?? word.fullDefinition ?? ""
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Get word part of speech
|
|
513
|
+
*/
|
|
514
|
+
export function getWordPartOfSpeech(word: GLOSTWord): string {
|
|
515
|
+
return word.metadata?.partOfSpeech ?? "";
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Get word difficulty
|
|
520
|
+
*/
|
|
521
|
+
export function getWordDifficulty(word: GLOSTWord): string {
|
|
522
|
+
return word.difficulty ?? word.extras?.metadata?.difficulty ?? "";
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Get sentence translation
|
|
527
|
+
*/
|
|
528
|
+
export function getSentenceTranslation(
|
|
529
|
+
sentence: GLOSTSentence,
|
|
530
|
+
language = "en",
|
|
531
|
+
): string | null {
|
|
532
|
+
if (sentence.extras?.translations?.[language]) {
|
|
533
|
+
return sentence.extras.translations[language];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Fallback: build from word meanings
|
|
537
|
+
const words = getWordsFromSentence(sentence);
|
|
538
|
+
const wordMeanings = words
|
|
539
|
+
.map((word) => getWordMeaning(word))
|
|
540
|
+
.filter(Boolean)
|
|
541
|
+
.join(" ");
|
|
542
|
+
|
|
543
|
+
return wordMeanings || null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ============================================================================
|
|
547
|
+
// Content Statistics Utilities
|
|
548
|
+
// ============================================================================
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Generic paragraph structure for word count calculation
|
|
552
|
+
* This interface allows converting external paragraph structures to GLOST format
|
|
553
|
+
*/
|
|
554
|
+
export type ParagraphLike = {
|
|
555
|
+
sentences: Array<{
|
|
556
|
+
sentence: string;
|
|
557
|
+
translation?: string;
|
|
558
|
+
}>;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Convert a paragraph-like structure to GLOST format for word count calculation
|
|
563
|
+
* This is a minimal adapter that only converts what's needed for word counting
|
|
564
|
+
*
|
|
565
|
+
* @param paragraph - Paragraph structure with sentences containing text and optional translations
|
|
566
|
+
* @returns GLOST paragraph node
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* ```ts
|
|
570
|
+
* const paragraph = {
|
|
571
|
+
* sentences: [
|
|
572
|
+
* { sentence: "Hello", translation: "สวัสดี" },
|
|
573
|
+
* { sentence: "World", translation: "โลก" }
|
|
574
|
+
* ]
|
|
575
|
+
* };
|
|
576
|
+
* const mtstParagraph = adaptParagraphLikeToGLOST(paragraph);
|
|
577
|
+
* const wordCount = getGLOSTWordCount(mtstParagraph);
|
|
578
|
+
* ```
|
|
579
|
+
*/
|
|
580
|
+
export function adaptParagraphLikeToGLOST(
|
|
581
|
+
paragraph: ParagraphLike,
|
|
582
|
+
): GLOSTParagraph {
|
|
583
|
+
return {
|
|
584
|
+
type: "ParagraphNode",
|
|
585
|
+
children: paragraph.sentences.map((sentence) => ({
|
|
586
|
+
type: "SentenceNode",
|
|
587
|
+
lang: "unknown",
|
|
588
|
+
script: "unknown",
|
|
589
|
+
originalText: sentence.sentence,
|
|
590
|
+
children: [],
|
|
591
|
+
extras: {
|
|
592
|
+
translations: sentence.translation
|
|
593
|
+
? { en: sentence.translation }
|
|
594
|
+
: undefined,
|
|
595
|
+
},
|
|
596
|
+
})),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Calculate word count from GLOST content
|
|
602
|
+
* Counts words from sentence translations or original text
|
|
603
|
+
*
|
|
604
|
+
* @param content - GLOST paragraph, sentence, or root node
|
|
605
|
+
* @param language - Optional language code for translation preference (default: 'en')
|
|
606
|
+
* @returns Word count as a number, or undefined if content is empty
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* ```ts
|
|
610
|
+
* const wordCount = getGLOSTWordCount(paragraph, 'en');
|
|
611
|
+
* // Returns: 245
|
|
612
|
+
* ```
|
|
613
|
+
*/
|
|
614
|
+
export function getGLOSTWordCount(
|
|
615
|
+
content: GLOSTParagraph | GLOSTSentence | GLOSTRoot,
|
|
616
|
+
language = "en",
|
|
617
|
+
): number | undefined {
|
|
618
|
+
if (isGLOSTParagraph(content)) {
|
|
619
|
+
const sentences = getAllSentences(content);
|
|
620
|
+
if (sentences.length === 0) {
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return sentences.reduce((count, sentence) => {
|
|
625
|
+
const translation = getSentenceTranslation(sentence, language);
|
|
626
|
+
const text = translation || sentence.originalText || "";
|
|
627
|
+
return count + text.split(/\s+/).filter((word) => word.length > 0).length;
|
|
628
|
+
}, 0);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (isGLOSTSentence(content)) {
|
|
632
|
+
const translation = getSentenceTranslation(content, language);
|
|
633
|
+
const text = translation || content.originalText || "";
|
|
634
|
+
if (!text) {
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
return text.split(/\s+/).filter((word) => word.length > 0).length;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (isGLOSTRoot(content)) {
|
|
641
|
+
const paragraphs = getAllParagraphs(content);
|
|
642
|
+
if (paragraphs.length === 0) {
|
|
643
|
+
return undefined;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return paragraphs.reduce((count, paragraph) => {
|
|
647
|
+
const paragraphCount = getGLOSTWordCount(paragraph, language);
|
|
648
|
+
return count + (paragraphCount ?? 0);
|
|
649
|
+
}, 0);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return undefined;
|
|
653
|
+
}
|