tycho-components 0.40.4 → 0.40.6

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.
@@ -0,0 +1,20 @@
1
+ import { EdgeDefinition, NodeDefinition, Position } from 'cytoscape';
2
+ import { Conllu, ConlluToken, Struct } from '../../configs';
3
+ export type CytoscapeTree = {
4
+ nodes: NodeDefinition[];
5
+ edges: EdgeDefinition[];
6
+ pan?: Position;
7
+ };
8
+ declare const ConlluUtils: {
9
+ convertToSentence: (conllText: string) => Conllu;
10
+ convertToConllu: (conllu: Conllu) => string;
11
+ convertToCytoscape: (conllu: Conllu) => CytoscapeTree;
12
+ getAsText: (conllu: Conllu) => string;
13
+ convertStructToConllu: (struct: Struct) => Conllu;
14
+ extractConlluTokens: (conllText: string) => ConlluToken[];
15
+ isNotEmptyConllu: (conllu: Conllu) => boolean;
16
+ deleteToken: (conllu: Conllu, tokenIndex: number) => Conllu;
17
+ getConlluTranslationFromStruct: (struct: Struct | undefined, lang: string) => string;
18
+ generateImageFromSVG: (svgElement: SVGSVGElement) => void;
19
+ };
20
+ export default ConlluUtils;
@@ -0,0 +1,271 @@
1
+ import { saveAs } from 'file-saver';
2
+ const convertStructToConllu = (struct) => {
3
+ return {
4
+ uid: struct.uid,
5
+ page: struct.page,
6
+ tokens: struct.conllu || [],
7
+ attributes: struct.attributes || {},
8
+ };
9
+ };
10
+ const getAsText = (conllu) => {
11
+ let value = '';
12
+ conllu.tokens.forEach((token) => {
13
+ value += `${token.form} `;
14
+ });
15
+ return value.trim();
16
+ };
17
+ const convertToCytoscape = (conllu) => {
18
+ const tokens = conllu.tokens.filter((t) => t.id?.indexOf('-') === -1);
19
+ if (tokens.length === 0)
20
+ return { nodes: [], edges: [] };
21
+ const nodes = [];
22
+ const edges = [];
23
+ const uids = [uuid()];
24
+ nodes.push({
25
+ data: {
26
+ id: uids[0],
27
+ root: true,
28
+ token: {
29
+ id: 0,
30
+ form: 'root',
31
+ },
32
+ },
33
+ });
34
+ for (const token of tokens) {
35
+ const thisUid = uuid();
36
+ uids.push(thisUid);
37
+ nodes.push({
38
+ data: {
39
+ id: thisUid,
40
+ token,
41
+ },
42
+ });
43
+ }
44
+ for (const token of tokens) {
45
+ if (token.head !== null && token.deprel) {
46
+ edges.push({
47
+ data: {
48
+ id: uuid(),
49
+ label: token.deprel,
50
+ target: uids[parseInt(token.id)],
51
+ source: uids[parseInt(token.head)],
52
+ },
53
+ });
54
+ }
55
+ }
56
+ return { nodes, edges };
57
+ };
58
+ const isNotEmptyConllu = (conllu) => {
59
+ return conllu && conllu.tokens.length > 0;
60
+ };
61
+ const convertToSentence = (conllText) => {
62
+ const lines = conllText.trim().split('\n');
63
+ const headerInfo = {};
64
+ const tokens = [];
65
+ for (const line of lines) {
66
+ if (line.startsWith('#')) {
67
+ const [key, value] = line.split('=');
68
+ if (value)
69
+ headerInfo[key.slice(2).trim()] = value.trim();
70
+ else
71
+ headerInfo[key.slice(2).trim()] = '';
72
+ }
73
+ else {
74
+ const fields = line.split('\t');
75
+ // check if id has '-', if positive this is a splitter
76
+ if (fields[0].indexOf('-') != -1)
77
+ continue;
78
+ const token = {
79
+ id: fields[0],
80
+ form: fields[1],
81
+ lemma: fields[2],
82
+ upos: fields[3],
83
+ xpos: fields[4] !== '_' ? fields[4] : '',
84
+ feats: fields[5] !== '_' ? fields[5] : '',
85
+ head: fields[6] ? fields[6] : '',
86
+ deprel: fields[7] ? fields[7] : '',
87
+ deps: fields[8] !== '_' ? fields[8] : '',
88
+ misc: fields[9] !== '_' ? fields[9] : '',
89
+ };
90
+ tokens.push(token);
91
+ }
92
+ }
93
+ const conllu = {
94
+ uid: uuid(),
95
+ page: '',
96
+ attributes: headerInfo || {},
97
+ tokens: tokens,
98
+ };
99
+ return conllu;
100
+ };
101
+ const convertToConllu = (conllu) => {
102
+ if (!isNotEmptyConllu(conllu))
103
+ return '';
104
+ const attributesLines = Object.entries(conllu.attributes)
105
+ .map(([key, value]) => `# ${key} = ${value}`)
106
+ .join('\n');
107
+ const tokenLines = conllu.tokens
108
+ .map((token) => [
109
+ token.id,
110
+ token.form,
111
+ token.lemma,
112
+ token.upos,
113
+ token.xpos,
114
+ token.feats,
115
+ token.head,
116
+ token.deprel,
117
+ token.deps === '' ? '_' : token.deps,
118
+ token.misc,
119
+ ].join('\t'))
120
+ .join('\n');
121
+ return [attributesLines, tokenLines].join('\n');
122
+ };
123
+ /**
124
+ * Deletes a token from the Conllu structure and updates all related references.
125
+ * - Updates token IDs to maintain sequential order (1, 2, 3, ...)
126
+ * - Updates HEAD references: if HEAD points to deleted token, points to previous token (or first if deleted was first)
127
+ * - Decrements HEAD references that point to tokens after the deleted one
128
+ */
129
+ const deleteToken = (conllu, tokenIndex) => {
130
+ if (tokenIndex < 0 || tokenIndex >= conllu.tokens.length) {
131
+ return conllu;
132
+ }
133
+ // Get the ID of the token being deleted (as number for comparison)
134
+ const deletedToken = conllu.tokens[tokenIndex];
135
+ const deletedTokenId = deletedToken.id;
136
+ const deletedTokenIdNum = Number(deletedTokenId);
137
+ // Calculate the new ID of the token before the deleted one
138
+ // After deletion and renumbering, if we delete at index i:
139
+ // - Token at index i-1 will have new ID = i (which is String(i))
140
+ // - If deleting first token (index 0), the new first token will have ID "1"
141
+ const previousTokenNewId = tokenIndex > 0 ? String(tokenIndex) : conllu.tokens.length > 1 ? '1' : '0';
142
+ // Remove the token at the specified index
143
+ const updatedTokens = conllu.tokens.filter((_, index) => index !== tokenIndex);
144
+ // Update IDs to be sequential (1, 2, 3, ...)
145
+ const tokensWithUpdatedIds = updatedTokens.map((token, index) => ({
146
+ ...token,
147
+ id: String(index + 1),
148
+ }));
149
+ // Update HEAD references based on the old IDs before deletion
150
+ const tokensWithUpdatedHeads = tokensWithUpdatedIds.map((token, newIndex) => {
151
+ // Get the original token (before ID update) to check its original HEAD
152
+ const originalToken = updatedTokens[newIndex];
153
+ // Skip if HEAD is empty, null, or '_'
154
+ if (!originalToken.head ||
155
+ originalToken.head === '_' ||
156
+ originalToken.head === '') {
157
+ return token;
158
+ }
159
+ const headIdNum = Number(originalToken.head);
160
+ // If HEAD points to the deleted token, point to the previous token (or first if deleted was first)
161
+ if (originalToken.head === deletedTokenId) {
162
+ return {
163
+ ...token,
164
+ head: previousTokenNewId,
165
+ };
166
+ }
167
+ // If HEAD points to a token after the deleted one, decrement it by 1
168
+ if (headIdNum > deletedTokenIdNum) {
169
+ return {
170
+ ...token,
171
+ head: String(headIdNum - 1),
172
+ };
173
+ }
174
+ // If HEAD points to a token before the deleted one, no change needed
175
+ // But we need to ensure the HEAD value is still valid after renumbering
176
+ // Since tokens before the deleted one keep their relative positions, their IDs don't change
177
+ // So we can keep the original HEAD value
178
+ return token;
179
+ });
180
+ return {
181
+ ...conllu,
182
+ tokens: tokensWithUpdatedHeads,
183
+ };
184
+ };
185
+ const uuid = () => 'xxxxxxxx'.replace(/[xy]/g, (c) => {
186
+ /* eslint-disable */
187
+ const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
188
+ /* eslint-enable */
189
+ return v.toString(16);
190
+ });
191
+ const extractConlluTokens = (conllText) => {
192
+ const lines = conllText.trim().split('\n');
193
+ const headerInfo = {};
194
+ const tokens = [];
195
+ for (const line of lines) {
196
+ if (line.startsWith('#')) {
197
+ const [key, value] = line.split('=');
198
+ if (value)
199
+ headerInfo[key.slice(2).trim()] = value.trim();
200
+ else
201
+ headerInfo[key.slice(2).trim()] = '';
202
+ }
203
+ else {
204
+ const fields = line.split('\t');
205
+ // check if id has '-', if positive this is a splitter
206
+ if (fields[0].indexOf('-') != -1)
207
+ continue;
208
+ const token = {
209
+ id: fields[0],
210
+ form: fields[1],
211
+ lemma: fields[2],
212
+ upos: fields[3],
213
+ xpos: fields[4] !== '_' ? fields[4] : '',
214
+ feats: fields[5] !== '_' ? fields[5] : '',
215
+ head: fields[6] ? fields[6] : '',
216
+ deprel: fields[7] ? fields[7] : '',
217
+ deps: fields[8] !== '_' ? fields[8] : '',
218
+ misc: fields[9] !== '_' ? fields[9] : '',
219
+ };
220
+ tokens.push(token);
221
+ }
222
+ }
223
+ return tokens;
224
+ };
225
+ const getConlluTranslationFromStruct = (struct, lang) => {
226
+ return struct?.attributes?.[lang] || '';
227
+ };
228
+ const generateImageFromSVG = (svgElement) => {
229
+ const canvas = document.createElement('canvas');
230
+ const context = canvas.getContext('2d');
231
+ if (!context) {
232
+ throw new Error('Unable to get canvas context');
233
+ }
234
+ const { width, height } = svgElement.getBBox();
235
+ canvas.width = width;
236
+ canvas.height = height;
237
+ // Convert SVG element to data URL
238
+ const svgData = new XMLSerializer().serializeToString(svgElement);
239
+ const svgBlob = new Blob([svgData], {
240
+ type: 'image/svg+xml;charset=utf-8',
241
+ });
242
+ const url = URL.createObjectURL(svgBlob);
243
+ const img = new Image();
244
+ img.crossOrigin = 'anonymous'; // handle cross-origin issues
245
+ img.onload = async () => {
246
+ context.fillStyle = 'white';
247
+ context.fillRect(0, 0, width, height);
248
+ context.drawImage(img, 0, 0, width, height);
249
+ URL.revokeObjectURL(url);
250
+ // Convert canvas to JPEG blob
251
+ canvas.toBlob((blob) => {
252
+ if (blob === null)
253
+ return;
254
+ saveAs(blob, 'conllu.jpg');
255
+ }, 'image/jpeg', 1.0);
256
+ };
257
+ img.src = url;
258
+ };
259
+ const ConlluUtils = {
260
+ convertToSentence,
261
+ convertToConllu,
262
+ convertToCytoscape,
263
+ getAsText,
264
+ convertStructToConllu,
265
+ extractConlluTokens,
266
+ isNotEmptyConllu,
267
+ deleteToken,
268
+ getConlluTranslationFromStruct,
269
+ generateImageFromSVG,
270
+ };
271
+ export default ConlluUtils;
@@ -9,6 +9,7 @@ export { EDITION_TOOLS, default as ToolsUtils, type PlatformTools, } from './Too
9
9
  export type { OpenDocumentParams, RedirectPageParams } from './ToolsUtils';
10
10
  export { default as UsabilityUtils } from './UsabilityUtils';
11
11
  export { conlluViewerConfigurations } from './conllu/ConlluViewerConstants';
12
+ export { default as ConlluUtils } from './conllu/ConlluUtils';
12
13
  export { default as ConlluViewerConverter, calculateConlluTextWidth, throwConlluError, } from './conllu/ConlluViewerConverter';
13
14
  export type { ConlluViewerAnchor, ConlluViewerDependency, ConlluViewerGraph, ConlluViewerItem, ConlluViewerMulti, } from './conllu/ConlluViewerGraph';
14
15
  export { normalizeCorpusFromApi } from './normalizeCorpusFromApi';
@@ -8,5 +8,6 @@ export { default as SentenceUtils } from './SentenceUtils';
8
8
  export { EDITION_TOOLS, default as ToolsUtils, } from './ToolsUtils';
9
9
  export { default as UsabilityUtils } from './UsabilityUtils';
10
10
  export { conlluViewerConfigurations } from './conllu/ConlluViewerConstants';
11
+ export { default as ConlluUtils } from './conllu/ConlluUtils';
11
12
  export { default as ConlluViewerConverter, calculateConlluTextWidth, throwConlluError, } from './conllu/ConlluViewerConverter';
12
13
  export { normalizeCorpusFromApi } from './normalizeCorpusFromApi';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tycho-components",
3
3
  "private": false,
4
- "version": "0.40.4",
4
+ "version": "0.40.6",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {