word-aligner 0.4.0 → 1.0.1-alpha

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.
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.getMorphLocalizationKeysGreek = exports.getMorphLocalizationKeysHebrewAramaic = exports.getMorphLocalizationKeys = undefined;
6
+ exports.getMorphLocalizationKeysGreekSR = exports.getMorphLocalizationKeysGreek = exports.getMorphLocalizationKeysHebrewAramaic = exports.getMorphLocalizationKeys = undefined;
7
7
 
8
8
  var _getIterator2 = require('babel-runtime/core-js/get-iterator');
9
9
 
@@ -29,6 +29,9 @@ var getMorphLocalizationKeys = exports.getMorphLocalizationKeys = function getMo
29
29
 
30
30
  case 'gr':
31
31
  default:
32
+ if (morph && morph.length === 12) {
33
+ return getMorphLocalizationKeysGreekSR(morph);
34
+ }
32
35
  return getMorphLocalizationKeysGreek(morph);
33
36
  }
34
37
  };
@@ -151,4 +154,53 @@ var getMorphLocalizationKeysGreek = exports.getMorphLocalizationKeysGreek = func
151
154
  } // unknown code, prefixing with '*'
152
155
  });
153
156
  return morphKeys;
157
+ };
158
+ /**
159
+ * @description - Get a list of all the localization keys for a morph string in Greek
160
+ * @param {String} morph - the morph string, e.g. Gr,N,,,,,GMS,
161
+ * @return {Array} - List of localization keys (unknown codes are prefixed with `*`)
162
+ */
163
+ var getMorphLocalizationKeysGreekSR = exports.getMorphLocalizationKeysGreekSR = function getMorphLocalizationKeysGreekSR(morph) {
164
+ if (!morph || typeof morph !== 'string' || !morph.trim().length) {
165
+ return [];
166
+ }
167
+
168
+ var morphKeys = [];
169
+ // Will parsed out the morph string to its 12 places, the 1st being language,
170
+ // 2nd always empty, 3rd role, 4th type, and so on
171
+ var regex = /([A-Z0-9,.][a-z]*)/g; // Delimited by boundry of a comma, period, or uppercase letter
172
+ var codes = morph.match(regex).map(function (code) {
173
+ return [',', '.'].includes(code) ? null : code;
174
+ });
175
+ if (codes.length < 3) {
176
+ return morph;
177
+ }
178
+
179
+ var morpMapGrk = _morphCodeLocalizationMap.morphCodeLocalizationMapSrGrk;
180
+ if (morpMapGrk[2].hasOwnProperty(codes[2])) {
181
+ morphKeys.push(morpMapGrk[2][codes[2]].key);
182
+ } else {
183
+ morphKeys.push('*' + codes[2]); // no known localization key, so prefixing with '*'
184
+ }
185
+ if (codes[4]) {
186
+ var col2 = morpMapGrk[2];
187
+ var col2Form = col2[codes[2]];
188
+ if (col2.hasOwnProperty(codes[2]) && col2Form[4] && col2Form[4].hasOwnProperty(codes[4])) {
189
+ morphKeys.push(col2Form[4][codes[4]]);
190
+ } else {
191
+ morphKeys.push('*' + codes[4]);
192
+ } // unknown type, prefixing with '*'
193
+ }
194
+ codes.forEach(function (code, index) {
195
+ // 0 and 1 are ignored, already did 2 and 3 above
196
+ if (index < 5 || !code) {
197
+ return;
198
+ }
199
+ if (morpMapGrk[index].hasOwnProperty(code)) {
200
+ morphKeys.push(morpMapGrk[index][code]);
201
+ } else {
202
+ morphKeys.push('*' + code);
203
+ } // unknown code, prefixing with '*'
204
+ });
205
+ return morphKeys;
154
206
  };
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.morphCodeLocalizationMapGrk = exports.morphCodeLocalizationMapAr = exports.morphCodeLocalizationMapHeb = undefined;
6
+ exports.morphCodeLocalizationMapGrk = exports.morphCodeLocalizationMapSrGrk = exports.morphCodeLocalizationMapAr = exports.morphCodeLocalizationMapHeb = undefined;
7
7
 
8
8
  var _lodash = require('lodash');
9
9
 
@@ -190,6 +190,97 @@ morphCodeLocalizationMapAr.verb_stems = {
190
190
  G: 'ittaphal'
191
191
  };
192
192
 
193
+ // These reflect the columns on for the SR on pages 9 and 10 of: https://greekcntr.org/resources/NTGRG.pdf
194
+ var formVI = { // Form for VI
195
+ I: 'indicative',
196
+ M: 'imperative',
197
+ S: 'subjunctive',
198
+ O: 'optative',
199
+ N: 'infinitive',
200
+ P: 'participle'
201
+ };
202
+
203
+ var formNADX = { // Form for NADX
204
+ C: 'comparative',
205
+ S: 'superlatives',
206
+ D: 'diminutive',
207
+ I: 'indeclinable'
208
+ };
209
+
210
+ var morphCodeLocalizationMapSrGrk = exports.morphCodeLocalizationMapSrGrk = {
211
+ 2: { // Function
212
+ N: {
213
+ key: 'noun',
214
+ 4: formNADX
215
+ },
216
+ R: {
217
+ key: 'pronoun'
218
+ },
219
+ A: {
220
+ key: 'adjective',
221
+ 4: formNADX
222
+ },
223
+ V: {
224
+ key: 'verb',
225
+ 4: formVI
226
+ },
227
+ D: {
228
+ key: 'adverb',
229
+ 4: formNADX
230
+ },
231
+ P: {
232
+ key: 'preposition'
233
+ },
234
+ C: {
235
+ key: 'conjunction'
236
+ },
237
+ I: {
238
+ key: 'interjection',
239
+ 4: formVI
240
+ },
241
+ X: {
242
+ key: 'determiner', // new
243
+ 4: formNADX
244
+ }
245
+ },
246
+ 5: { // Tense
247
+ P: 'present',
248
+ I: 'imperfect',
249
+ F: 'future',
250
+ A: 'aorist',
251
+ E: 'perfect',
252
+ L: 'pluperfect'
253
+ },
254
+ 6: { // Voice
255
+ A: 'active',
256
+ M: 'middle',
257
+ P: 'passive'
258
+ },
259
+ 7: { // Person
260
+ 1: 'first',
261
+ 2: 'second',
262
+ 3: 'third'
263
+ },
264
+ 8: { // Case
265
+ N: 'nominative',
266
+ G: 'genitive',
267
+ D: 'dative',
268
+ A: 'accusative',
269
+ V: 'vocative'
270
+ },
271
+ 9: { // Gender
272
+ M: 'masculine',
273
+ F: 'feminine',
274
+ N: 'neuter',
275
+ A: 'any' // new
276
+ },
277
+ 10: { // Number
278
+ S: 'singular',
279
+ P: 'plural',
280
+ A: 'any' // new
281
+ }
282
+ };
283
+
193
284
  // These reflect the columns on page 55 of https://greekcntr.org/downloads/project.pdf
194
285
  // This helps us translate codes starting and the 3rd place (the 2nd index) of a morph string
195
286
  // The numbered keys are the index of that code in the string, where the letter index is the code
@@ -528,17 +528,29 @@ var getWordListFromVerseObjectArray = exports.getWordListFromVerseObjectArray =
528
528
  return wordList;
529
529
  };
530
530
 
531
- var addContentAttributeToChildren = function addContentAttributeToChildren(childrens, parentObject, grandParentObject) {
531
+ /**
532
+ * maps original language content to each child and flattens them into array, recursive processing
533
+ * @param {Array} childrens - list to process
534
+ * @param {Array} ancestors - ordered list of all original language ancestors
535
+ * @return {[]} returns flat array of all children
536
+ */
537
+ var addContentAttributeToChildren = function addContentAttributeToChildren(childrens, ancestors) {
532
538
  var childrensWithAttribute = [];
533
539
 
534
- for (var i = 0; i < childrens.length; i++) {
540
+ for (var i = 0, lc = childrens.length; i < lc; i++) {
535
541
  var child = childrens[i];
536
542
  if (child.children) {
537
- child = addContentAttributeToChildren(child.children, child, parentObject);
538
- } else if (!child.content && parentObject.content) {
539
- var childrenContent = [parentObject];
540
- if (grandParentObject) childrenContent.push(grandParentObject);
541
- child.content = childrenContent;
543
+ child = addContentAttributeToChildren(child.children, [child].concat((0, _toConsumableArray3.default)(ancestors)));
544
+ } else if (ancestors[0].content) {
545
+ if (!child.content) {
546
+ child.content = [];
547
+ }
548
+ for (var j = 0, la = ancestors.length; j < la; j++) {
549
+ var ancestor = ancestors[j];
550
+ if (ancestor.content) {
551
+ child.content.push(ancestor);
552
+ }
553
+ }
542
554
  }
543
555
  childrensWithAttribute.push(child);
544
556
  }
@@ -552,7 +564,7 @@ var addContentAttributeToChildren = function addContentAttributeToChildren(child
552
564
  * @param {array} words - output array that will be filled with flattened verseObjects
553
565
  */
554
566
  var flattenVerseObjects = function flattenVerseObjects(verse, words) {
555
- for (var i = 0; i < verse.length; i++) {
567
+ for (var i = 0, l = verse.length; i < l; i++) {
556
568
  var object = verse[i];
557
569
  if (object) {
558
570
  if (object.type === 'word') {
@@ -561,7 +573,7 @@ var flattenVerseObjects = function flattenVerseObjects(verse, words) {
561
573
  } else if (object.type === 'milestone') {
562
574
  // get children of milestone
563
575
  // add content attibute to children
564
- var newObject = addContentAttributeToChildren(object.children, object);
576
+ var newObject = addContentAttributeToChildren(object.children, [object]);
565
577
  flattenVerseObjects(newObject, words);
566
578
  } else {
567
579
  words.push(object);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "word-aligner",
3
- "version": "0.4.0",
3
+ "version": "1.0.1-alpha",
4
4
  "description": "A library for handling word alignment",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -51,6 +51,7 @@
51
51
  "eslint": "^5.12.1",
52
52
  "eslint-config-google": "^0.12.0",
53
53
  "eslint-plugin-jest": "^22.1.3",
54
+ "json-stringify-safe": "5.0.1",
54
55
  "jest": "^23.6.0",
55
56
  "ospath": "1.2.2",
56
57
  "usfm-js": "2.1.0"
package/src/js/aligner.js CHANGED
@@ -29,14 +29,14 @@ export const hasAlignments = (alignments) => {
29
29
  * @return {Array} - sorted array of verseObjects to be used for verseText of targetLanguage
30
30
  */
31
31
  export const merge = (alignments, wordBank, verseString,
32
- useVerseText = false) => {
32
+ useVerseText = false) => {
33
33
  // get the definitive list of verseObjects from the verse, unaligned but in order
34
34
  const {newVerseObjects: unalignedOrdered, wordMap} =
35
35
  VerseObjectUtils.getOrderedVerseObjectsFromString(verseString);
36
36
  // assign verseObjects with unaligned objects to be replaced with aligned ones
37
37
  // check each word in the verse string is also in the word bank or alignments
38
38
  const verseObjectsNotInAlignmentData = verseStringWordsContainedInAlignments(
39
- alignments, wordBank, wordMap);
39
+ alignments, wordBank, wordMap);
40
40
  if (verseObjectsNotInAlignmentData.length > 0) {
41
41
  if (hasAlignments(alignments)) { // if verse has some alignments
42
42
  const verseWordsJoined = verseObjectsNotInAlignmentData.map(({text}) => text).join(', ');
@@ -53,9 +53,9 @@ export const merge = (alignments, wordBank, verseString,
53
53
  for (let i = 0; i < wbLen; i++) {
54
54
  const bottomWord = wordBank[i];
55
55
  const verseObject = VerseObjectUtils.wordVerseObjectFromBottomWord(
56
- bottomWord);
56
+ bottomWord);
57
57
  const index = VerseObjectUtils.indexOfVerseObject(
58
- wordMap, verseObject);
58
+ wordMap, verseObject);
59
59
  if (index > -1) {
60
60
  const location = wordMap[index];
61
61
  location.array[location.pos] = verseObject;
@@ -132,7 +132,7 @@ export const merge = (alignments, wordBank, verseString,
132
132
  * the given alignments
133
133
  */
134
134
  export function verseStringWordsContainedInAlignments(
135
- alignments, wordBank, wordMap) {
135
+ alignments, wordBank, wordMap) {
136
136
  const unalignedMap = wordMap.filter((wordItem) => {
137
137
  const verseObject = wordItem.array[wordItem.pos];
138
138
  const checkIfWordMatches = function(verseObject) {
@@ -251,7 +251,7 @@ export const orderAlignments = function(alignmentVerse, alignmentUnOrdered) {
251
251
  let orderedObjects = null;
252
252
  if (typeof alignmentVerse === 'string') {
253
253
  orderedObjects = VerseObjectUtils.getOrderedVerseObjectsFromString(
254
- alignmentVerse);
254
+ alignmentVerse);
255
255
  } else {
256
256
  orderedObjects = VerseObjectUtils.getOrderedVerseObjects(alignmentVerse);
257
257
  }
@@ -288,7 +288,7 @@ export const orderAlignments = function(alignmentVerse, alignmentUnOrdered) {
288
288
  }
289
289
  if (index < 0) { // if still not found in topWords, it's an unaligned topWord
290
290
  const wordObject = VerseObjectUtils.alignmentObjectFromVerseObject(
291
- nextWord);
291
+ nextWord);
292
292
  alignment.push({topWords: [wordObject], bottomWords: []});
293
293
  }
294
294
  }
@@ -309,7 +309,7 @@ export const orderAlignments = function(alignmentVerse, alignmentUnOrdered) {
309
309
  export const addVerseObjectToAlignment = (verseObject, alignment) => {
310
310
  if (verseObject.type === 'milestone' && verseObject.children.length > 0) {
311
311
  const wordObject = VerseObjectUtils.alignmentObjectFromVerseObject(
312
- verseObject);
312
+ verseObject);
313
313
  const duplicate = alignment.topWords.find(function(obj) {
314
314
  return (obj.word === wordObject.word) &&
315
315
  (obj.occurrence === wordObject.occurrence);
@@ -322,7 +322,7 @@ export const addVerseObjectToAlignment = (verseObject, alignment) => {
322
322
  });
323
323
  } else if (verseObject.type === 'word' && !verseObject.children) {
324
324
  const wordObject = VerseObjectUtils.alignmentObjectFromVerseObject(
325
- verseObject);
325
+ verseObject);
326
326
  alignment.bottomWords.push(wordObject);
327
327
  }
328
328
  };
@@ -457,7 +457,7 @@ export const generateWordBank = (verseData) => {
457
457
  * @return {{alignments, wordBank}} - Reset alignments data
458
458
  */
459
459
  export const getBlankAlignmentDataForVerse = (
460
- ugntVerse, targetLanguageVerse) => {
460
+ ugntVerse, targetLanguageVerse) => {
461
461
  const alignments = generateBlankAlignments(ugntVerse);
462
462
  const wordBank = generateWordBank(targetLanguageVerse);
463
463
  return {alignments, wordBank};
@@ -1,5 +1,10 @@
1
1
  /* eslint-disable no-use-before-define */
2
- import {morphCodeLocalizationMapGrk, morphCodeLocalizationMapAr, morphCodeLocalizationMapHeb} from './morphCodeLocalizationMap';
2
+ import {
3
+ morphCodeLocalizationMapGrk,
4
+ morphCodeLocalizationMapAr,
5
+ morphCodeLocalizationMapHeb,
6
+ morphCodeLocalizationMapSrGrk
7
+ } from './morphCodeLocalizationMap';
3
8
 
4
9
  /**
5
10
  * @description - Get a list of all the localization keys for a morph string in Greek
@@ -17,6 +22,9 @@ export const getMorphLocalizationKeys = (morph) => {
17
22
 
18
23
  case 'gr':
19
24
  default:
25
+ if (morph && morph.length === 12) {
26
+ return getMorphLocalizationKeysGreekSR(morph);
27
+ }
20
28
  return getMorphLocalizationKeysGreek(morph);
21
29
  }
22
30
  };
@@ -115,3 +123,51 @@ export const getMorphLocalizationKeysGreek = (morph) => {
115
123
  });
116
124
  return morphKeys;
117
125
  };
126
+ /**
127
+ * @description - Get a list of all the localization keys for a morph string in Greek
128
+ * @param {String} morph - the morph string, e.g. Gr,N,,,,,GMS,
129
+ * @return {Array} - List of localization keys (unknown codes are prefixed with `*`)
130
+ */
131
+ export const getMorphLocalizationKeysGreekSR = (morph) => {
132
+ if (!morph || typeof morph !== 'string' || !morph.trim().length) {
133
+ return [];
134
+ }
135
+
136
+ const morphKeys = [];
137
+ // Will parsed out the morph string to its 12 places, the 1st being language,
138
+ // 2nd always empty, 3rd role, 4th type, and so on
139
+ const regex = /([A-Z0-9,.][a-z]*)/g; // Delimited by boundry of a comma, period, or uppercase letter
140
+ const codes = morph.match(regex).map((code) => [',', '.'].includes(code) ? null : code);
141
+ if (codes.length < 3) {
142
+ return morph;
143
+ }
144
+
145
+ const morpMapGrk = morphCodeLocalizationMapSrGrk;
146
+ if (morpMapGrk[2].hasOwnProperty(codes[2])) {
147
+ morphKeys.push(morpMapGrk[2][codes[2]].key);
148
+ } else {
149
+ morphKeys.push('*' + codes[2]); // no known localization key, so prefixing with '*'
150
+ }
151
+ if (codes[4]) {
152
+ const col2 = morpMapGrk[2];
153
+ const col2Form = col2[codes[2]];
154
+ if (col2.hasOwnProperty(codes[2]) && col2Form[4] && col2Form[4].hasOwnProperty(codes[4])) {
155
+ morphKeys.push(col2Form[4][codes[4]]);
156
+ } else {
157
+ morphKeys.push('*' + codes[4]);
158
+ } // unknown type, prefixing with '*'
159
+ }
160
+ codes.forEach((code, index) => {
161
+ // 0 and 1 are ignored, already did 2 and 3 above
162
+ if (index < 5 || !code) {
163
+ return;
164
+ }
165
+ if (morpMapGrk[index].hasOwnProperty(code)) {
166
+ morphKeys.push(morpMapGrk[index][code]);
167
+ } else {
168
+ morphKeys.push('*' + code);
169
+ } // unknown code, prefixing with '*'
170
+ });
171
+ return morphKeys;
172
+ };
173
+
@@ -183,6 +183,97 @@ morphCodeLocalizationMapAr.verb_stems = {
183
183
  G: 'ittaphal',
184
184
  };
185
185
 
186
+ // These reflect the columns on for the SR on pages 9 and 10 of: https://greekcntr.org/resources/NTGRG.pdf
187
+ const formVI = { // Form for VI
188
+ I: 'indicative',
189
+ M: 'imperative',
190
+ S: 'subjunctive',
191
+ O: 'optative',
192
+ N: 'infinitive',
193
+ P: 'participle',
194
+ };
195
+
196
+ const formNADX = { // Form for NADX
197
+ C: 'comparative',
198
+ S: 'superlatives',
199
+ D: 'diminutive',
200
+ I: 'indeclinable',
201
+ };
202
+
203
+ export const morphCodeLocalizationMapSrGrk = {
204
+ 2: { // Function
205
+ N: {
206
+ key: 'noun',
207
+ 4: formNADX,
208
+ },
209
+ R: {
210
+ key: 'pronoun',
211
+ },
212
+ A: {
213
+ key: 'adjective',
214
+ 4: formNADX,
215
+ },
216
+ V: {
217
+ key: 'verb',
218
+ 4: formVI,
219
+ },
220
+ D: {
221
+ key: 'adverb',
222
+ 4: formNADX,
223
+ },
224
+ P: {
225
+ key: 'preposition',
226
+ },
227
+ C: {
228
+ key: 'conjunction',
229
+ },
230
+ I: {
231
+ key: 'interjection',
232
+ 4: formVI,
233
+ },
234
+ X: {
235
+ key: 'determiner', // new
236
+ 4: formNADX,
237
+ },
238
+ },
239
+ 5: { // Tense
240
+ P: 'present',
241
+ I: 'imperfect',
242
+ F: 'future',
243
+ A: 'aorist',
244
+ E: 'perfect',
245
+ L: 'pluperfect',
246
+ },
247
+ 6: { // Voice
248
+ A: 'active',
249
+ M: 'middle',
250
+ P: 'passive',
251
+ },
252
+ 7: { // Person
253
+ 1: 'first',
254
+ 2: 'second',
255
+ 3: 'third',
256
+ },
257
+ 8: { // Case
258
+ N: 'nominative',
259
+ G: 'genitive',
260
+ D: 'dative',
261
+ A: 'accusative',
262
+ V: 'vocative',
263
+ },
264
+ 9: { // Gender
265
+ M: 'masculine',
266
+ F: 'feminine',
267
+ N: 'neuter',
268
+ A: 'any', // new
269
+ },
270
+ 10: { // Number
271
+ S: 'singular',
272
+ P: 'plural',
273
+ A: 'any', // new
274
+ },
275
+ };
276
+
186
277
  // These reflect the columns on page 55 of https://greekcntr.org/downloads/project.pdf
187
278
  // This helps us translate codes starting and the 3rd place (the 2nd index) of a morph string
188
279
  // The numbered keys are the index of that code in the string, where the letter index is the code
@@ -428,17 +428,29 @@ export const getWordListFromVerseObjectArray = (verseObjects) => {
428
428
  return wordList;
429
429
  };
430
430
 
431
- const addContentAttributeToChildren = (childrens, parentObject, grandParentObject) => {
431
+ /**
432
+ * maps original language content to each child and flattens them into array, recursive processing
433
+ * @param {Array} childrens - list to process
434
+ * @param {Array} ancestors - ordered list of all original language ancestors
435
+ * @return {[]} returns flat array of all children
436
+ */
437
+ const addContentAttributeToChildren = (childrens, ancestors) => {
432
438
  const childrensWithAttribute = [];
433
439
 
434
- for (let i = 0; i < childrens.length; i++) {
440
+ for (let i = 0, lc = childrens.length; i < lc; i++) {
435
441
  let child = childrens[i];
436
442
  if (child.children) {
437
- child = addContentAttributeToChildren(child.children, child, parentObject);
438
- } else if (!child.content && parentObject.content) {
439
- const childrenContent = [parentObject];
440
- if (grandParentObject) childrenContent.push(grandParentObject);
441
- child.content = childrenContent;
443
+ child = addContentAttributeToChildren(child.children, [child, ...ancestors]);
444
+ } else if (ancestors[0].content) {
445
+ if (!child.content) {
446
+ child.content = [];
447
+ }
448
+ for (let j = 0, la = ancestors.length; j < la; j++) {
449
+ const ancestor = ancestors[j];
450
+ if (ancestor.content) {
451
+ child.content.push(ancestor);
452
+ }
453
+ }
442
454
  }
443
455
  childrensWithAttribute.push(child);
444
456
  }
@@ -452,7 +464,7 @@ const addContentAttributeToChildren = (childrens, parentObject, grandParentObjec
452
464
  * @param {array} words - output array that will be filled with flattened verseObjects
453
465
  */
454
466
  const flattenVerseObjects = (verse, words) => {
455
- for (let i = 0; i < verse.length; i++) {
467
+ for (let i = 0, l = verse.length; i < l; i++) {
456
468
  const object = verse[i];
457
469
  if (object) {
458
470
  if (object.type === 'word') {
@@ -460,8 +472,7 @@ const flattenVerseObjects = (verse, words) => {
460
472
  words.push(object);
461
473
  } else if (object.type === 'milestone') { // get children of milestone
462
474
  // add content attibute to children
463
- const newObject = addContentAttributeToChildren(object.children,
464
- object);
475
+ const newObject = addContentAttributeToChildren(object.children, [object]);
465
476
  flattenVerseObjects(newObject, words);
466
477
  } else {
467
478
  words.push(object);
@@ -1,4 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="Encoding" addBOMForNewFiles="with NO BOM" />
4
- </project>