@rsconcept/domain 1.0.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.
Files changed (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -0
  3. package/dist/cctext/index.d.ts +1 -0
  4. package/dist/cctext/index.js +42 -0
  5. package/dist/cctext/index.js.map +1 -0
  6. package/dist/cctext/language-api.d.ts +43 -0
  7. package/dist/cctext/language-api.js +252 -0
  8. package/dist/cctext/language-api.js.map +1 -0
  9. package/dist/cctext/language.d.ts +58 -0
  10. package/dist/cctext/language.js +44 -0
  11. package/dist/cctext/language.js.map +1 -0
  12. package/dist/graph/graph.d.ts +62 -0
  13. package/dist/graph/graph.js +385 -0
  14. package/dist/graph/graph.js.map +1 -0
  15. package/dist/graph/index.d.ts +1 -0
  16. package/dist/graph/index.js +384 -0
  17. package/dist/graph/index.js.map +1 -0
  18. package/dist/index.d.ts +28 -0
  19. package/dist/index.js +5851 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/library/folder-tree.d.ts +32 -0
  22. package/dist/library/folder-tree.js +136 -0
  23. package/dist/library/folder-tree.js.map +1 -0
  24. package/dist/library/index.d.ts +17 -0
  25. package/dist/library/index.js +2800 -0
  26. package/dist/library/index.js.map +1 -0
  27. package/dist/library/library-api.d.ts +6 -0
  28. package/dist/library/library-api.js +13 -0
  29. package/dist/library/library-api.js.map +1 -0
  30. package/dist/library/library.d.ts +56 -0
  31. package/dist/library/library.js +23 -0
  32. package/dist/library/library.js.map +1 -0
  33. package/dist/library/oss-api.d.ts +47 -0
  34. package/dist/library/oss-api.js +1105 -0
  35. package/dist/library/oss-api.js.map +1 -0
  36. package/dist/library/oss-layout-api.d.ts +36 -0
  37. package/dist/library/oss-layout-api.js +330 -0
  38. package/dist/library/oss-layout-api.js.map +1 -0
  39. package/dist/library/oss-layout.d.ts +25 -0
  40. package/dist/library/oss-layout.js +1 -0
  41. package/dist/library/oss-layout.js.map +1 -0
  42. package/dist/library/oss.d.ts +136 -0
  43. package/dist/library/oss.js +30 -0
  44. package/dist/library/oss.js.map +1 -0
  45. package/dist/library/rsengine.d.ts +116 -0
  46. package/dist/library/rsengine.js +2604 -0
  47. package/dist/library/rsengine.js.map +1 -0
  48. package/dist/library/rsform-api.d.ts +74 -0
  49. package/dist/library/rsform-api.js +879 -0
  50. package/dist/library/rsform-api.js.map +1 -0
  51. package/dist/library/rsform.d.ts +206 -0
  52. package/dist/library/rsform.js +32 -0
  53. package/dist/library/rsform.js.map +1 -0
  54. package/dist/library/rsmodel-api.d.ts +43 -0
  55. package/dist/library/rsmodel-api.js +836 -0
  56. package/dist/library/rsmodel-api.js.map +1 -0
  57. package/dist/library/rsmodel.d.ts +52 -0
  58. package/dist/library/rsmodel.js +25 -0
  59. package/dist/library/rsmodel.js.map +1 -0
  60. package/dist/library/structure-planner.d.ts +33 -0
  61. package/dist/library/structure-planner.js +481 -0
  62. package/dist/library/structure-planner.js.map +1 -0
  63. package/dist/parsing/ast.d.ts +49 -0
  64. package/dist/parsing/ast.js +93 -0
  65. package/dist/parsing/ast.js.map +1 -0
  66. package/dist/parsing/index.d.ts +3 -0
  67. package/dist/parsing/index.js +141 -0
  68. package/dist/parsing/index.js.map +1 -0
  69. package/dist/parsing/lezer-tree.d.ts +13 -0
  70. package/dist/parsing/lezer-tree.js +50 -0
  71. package/dist/parsing/lezer-tree.js.map +1 -0
  72. package/dist/rslang/api.d.ts +53 -0
  73. package/dist/rslang/api.js +846 -0
  74. package/dist/rslang/api.js.map +1 -0
  75. package/dist/rslang/ast-annotations.d.ts +18 -0
  76. package/dist/rslang/ast-annotations.js +56 -0
  77. package/dist/rslang/ast-annotations.js.map +1 -0
  78. package/dist/rslang/error.d.ts +85 -0
  79. package/dist/rslang/error.js +159 -0
  80. package/dist/rslang/error.js.map +1 -0
  81. package/dist/rslang/eval/calculator.d.ts +43 -0
  82. package/dist/rslang/eval/calculator.js +1639 -0
  83. package/dist/rslang/eval/calculator.js.map +1 -0
  84. package/dist/rslang/eval/evaluation-cache.d.ts +36 -0
  85. package/dist/rslang/eval/evaluation-cache.js +310 -0
  86. package/dist/rslang/eval/evaluation-cache.js.map +1 -0
  87. package/dist/rslang/eval/evaluator.d.ts +70 -0
  88. package/dist/rslang/eval/evaluator.js +1514 -0
  89. package/dist/rslang/eval/evaluator.js.map +1 -0
  90. package/dist/rslang/eval/value-api.d.ts +48 -0
  91. package/dist/rslang/eval/value-api.js +490 -0
  92. package/dist/rslang/eval/value-api.js.map +1 -0
  93. package/dist/rslang/eval/value.d.ts +36 -0
  94. package/dist/rslang/eval/value.js +118 -0
  95. package/dist/rslang/eval/value.js.map +1 -0
  96. package/dist/rslang/index.d.ts +17 -0
  97. package/dist/rslang/index.js +4314 -0
  98. package/dist/rslang/index.js.map +1 -0
  99. package/dist/rslang/labels.d.ts +16 -0
  100. package/dist/rslang/labels.js +315 -0
  101. package/dist/rslang/labels.js.map +1 -0
  102. package/dist/rslang/parser/expression-generator.d.ts +10 -0
  103. package/dist/rslang/parser/expression-generator.js +451 -0
  104. package/dist/rslang/parser/expression-generator.js.map +1 -0
  105. package/dist/rslang/parser/normalize.d.ts +11 -0
  106. package/dist/rslang/parser/normalize.js +507 -0
  107. package/dist/rslang/parser/normalize.js.map +1 -0
  108. package/dist/rslang/parser/parser.d.ts +5 -0
  109. package/dist/rslang/parser/parser.js +24 -0
  110. package/dist/rslang/parser/parser.js.map +1 -0
  111. package/dist/rslang/parser/parser.terms.d.ts +42 -0
  112. package/dist/rslang/parser/parser.terms.js +84 -0
  113. package/dist/rslang/parser/parser.terms.js.map +1 -0
  114. package/dist/rslang/parser/syntax-errors.d.ts +11 -0
  115. package/dist/rslang/parser/syntax-errors.js +403 -0
  116. package/dist/rslang/parser/syntax-errors.js.map +1 -0
  117. package/dist/rslang/parser/token.d.ts +79 -0
  118. package/dist/rslang/parser/token.js +95 -0
  119. package/dist/rslang/parser/token.js.map +1 -0
  120. package/dist/rslang/semantic/analyzer.d.ts +39 -0
  121. package/dist/rslang/semantic/analyzer.js +2604 -0
  122. package/dist/rslang/semantic/analyzer.js.map +1 -0
  123. package/dist/rslang/semantic/arguments-extractor.d.ts +42 -0
  124. package/dist/rslang/semantic/arguments-extractor.js +366 -0
  125. package/dist/rslang/semantic/arguments-extractor.js.map +1 -0
  126. package/dist/rslang/semantic/type-auditor.d.ts +73 -0
  127. package/dist/rslang/semantic/type-auditor.js +1570 -0
  128. package/dist/rslang/semantic/type-auditor.js.map +1 -0
  129. package/dist/rslang/semantic/typification-api.d.ts +27 -0
  130. package/dist/rslang/semantic/typification-api.js +320 -0
  131. package/dist/rslang/semantic/typification-api.js.map +1 -0
  132. package/dist/rslang/semantic/typification-parser.d.ts +12 -0
  133. package/dist/rslang/semantic/typification-parser.js +226 -0
  134. package/dist/rslang/semantic/typification-parser.js.map +1 -0
  135. package/dist/rslang/semantic/typification.d.ts +119 -0
  136. package/dist/rslang/semantic/typification.js +74 -0
  137. package/dist/rslang/semantic/typification.js.map +1 -0
  138. package/dist/rslang/semantic/value-auditor.d.ts +43 -0
  139. package/dist/rslang/semantic/value-auditor.js +523 -0
  140. package/dist/rslang/semantic/value-auditor.js.map +1 -0
  141. package/dist/rslang/semantic/value-class.d.ts +10 -0
  142. package/dist/rslang/semantic/value-class.js +9 -0
  143. package/dist/rslang/semantic/value-class.js.map +1 -0
  144. package/dist/rslang/typification-graph.d.ts +33 -0
  145. package/dist/rslang/typification-graph.js +311 -0
  146. package/dist/rslang/typification-graph.js.map +1 -0
  147. package/dist/shared/branded.d.ts +7 -0
  148. package/dist/shared/branded.js +1 -0
  149. package/dist/shared/branded.js.map +1 -0
  150. package/dist/shared/hash.d.ts +6 -0
  151. package/dist/shared/hash.js +18 -0
  152. package/dist/shared/hash.js.map +1 -0
  153. package/dist/shared/index.d.ts +2 -0
  154. package/dist/shared/index.js +18 -0
  155. package/dist/shared/index.js.map +1 -0
  156. package/package.json +184 -0
  157. package/src/cctext/index.ts +9 -0
  158. package/src/cctext/language-api.test.ts +149 -0
  159. package/src/cctext/language-api.ts +285 -0
  160. package/src/cctext/language.ts +80 -0
  161. package/src/graph/graph.test.ts +392 -0
  162. package/src/graph/graph.ts +433 -0
  163. package/src/graph/index.ts +1 -0
  164. package/src/index.ts +96 -0
  165. package/src/library/folder-tree.test.ts +47 -0
  166. package/src/library/folder-tree.ts +156 -0
  167. package/src/library/index.ts +46 -0
  168. package/src/library/library-api.test.ts +32 -0
  169. package/src/library/library-api.ts +11 -0
  170. package/src/library/library.ts +61 -0
  171. package/src/library/oss-api.ts +449 -0
  172. package/src/library/oss-layout-api.ts +377 -0
  173. package/src/library/oss-layout.ts +27 -0
  174. package/src/library/oss.ts +150 -0
  175. package/src/library/rsengine.ts +593 -0
  176. package/src/library/rsform-api.ts +533 -0
  177. package/src/library/rsform.ts +228 -0
  178. package/src/library/rsmodel-api.ts +340 -0
  179. package/src/library/rsmodel.ts +50 -0
  180. package/src/library/structure-planner.ts +143 -0
  181. package/src/parsing/ast.ts +136 -0
  182. package/src/parsing/index.ts +15 -0
  183. package/src/parsing/lezer-tree.ts +69 -0
  184. package/src/rslang/api.test.ts +116 -0
  185. package/src/rslang/api.ts +183 -0
  186. package/src/rslang/ast-annotations.ts +70 -0
  187. package/src/rslang/error.ts +129 -0
  188. package/src/rslang/eval/calculator.test.ts +124 -0
  189. package/src/rslang/eval/calculator.ts +121 -0
  190. package/src/rslang/eval/evaluation-cache.ts +257 -0
  191. package/src/rslang/eval/evaluator.test.ts +352 -0
  192. package/src/rslang/eval/evaluator.ts +935 -0
  193. package/src/rslang/eval/value-api.test.ts +105 -0
  194. package/src/rslang/eval/value-api.ts +444 -0
  195. package/src/rslang/eval/value.ts +102 -0
  196. package/src/rslang/index.ts +23 -0
  197. package/src/rslang/labels.ts +191 -0
  198. package/src/rslang/parser/expression-generator.test.ts +100 -0
  199. package/src/rslang/parser/expression-generator.ts +466 -0
  200. package/src/rslang/parser/normalize.test.ts +99 -0
  201. package/src/rslang/parser/normalize.ts +462 -0
  202. package/src/rslang/parser/parser.terms.ts +42 -0
  203. package/src/rslang/parser/parser.test.ts +153 -0
  204. package/src/rslang/parser/parser.ts +20 -0
  205. package/src/rslang/parser/rslang.grammar +251 -0
  206. package/src/rslang/parser/syntax-errors.ts +209 -0
  207. package/src/rslang/parser/token.ts +106 -0
  208. package/src/rslang/semantic/analyzer.test.ts +59 -0
  209. package/src/rslang/semantic/analyzer.ts +179 -0
  210. package/src/rslang/semantic/arguments-extractor.ts +327 -0
  211. package/src/rslang/semantic/type-auditor.test.ts +326 -0
  212. package/src/rslang/semantic/type-auditor.ts +1049 -0
  213. package/src/rslang/semantic/typification-api.test.ts +46 -0
  214. package/src/rslang/semantic/typification-api.ts +321 -0
  215. package/src/rslang/semantic/typification-parser.test.ts +50 -0
  216. package/src/rslang/semantic/typification-parser.ts +220 -0
  217. package/src/rslang/semantic/typification.ts +180 -0
  218. package/src/rslang/semantic/value-auditor.test.ts +206 -0
  219. package/src/rslang/semantic/value-auditor.ts +332 -0
  220. package/src/rslang/semantic/value-class.ts +11 -0
  221. package/src/rslang/typification-graph.ts +155 -0
  222. package/src/shared/branded.ts +6 -0
  223. package/src/shared/hash.ts +17 -0
  224. package/src/shared/index.ts +2 -0
@@ -0,0 +1,149 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { Grammeme } from './language';
4
+ import {
5
+ extractEntities,
6
+ generateNominalLexeme,
7
+ parseEntityReference,
8
+ parseGrammemes,
9
+ parseReference,
10
+ parseSyntacticReference,
11
+ resolveTextReferences,
12
+ wordFormEquals
13
+ } from './language-api';
14
+
15
+ describe('Testing wordform equality', () => {
16
+ it('empty input', () => {
17
+ expect(wordFormEquals({ text: '', grams: [] }, { text: '', grams: [] })).toEqual(true);
18
+ expect(wordFormEquals({ text: '', grams: [] }, { text: '11', grams: [] })).toEqual(true);
19
+ expect(wordFormEquals({ text: '11', grams: [] }, { text: '', grams: [] })).toEqual(true);
20
+ expect(wordFormEquals({ text: '11', grams: [] }, { text: '42', grams: [] })).toEqual(true);
21
+ expect(wordFormEquals({ text: '', grams: ['nomn'] }, { text: '', grams: [] })).toEqual(false);
22
+ expect(wordFormEquals({ text: '11', grams: ['nomn'] }, { text: '11', grams: [] })).toEqual(false);
23
+ expect(wordFormEquals({ text: '', grams: [] }, { text: '', grams: ['nomn'] })).toEqual(false);
24
+ expect(wordFormEquals({ text: '11', grams: [] }, { text: '11', grams: ['nomn'] })).toEqual(false);
25
+ });
26
+
27
+ it('regular grammemes', () => {
28
+ expect(wordFormEquals({ text: '', grams: ['nomn'] }, { text: '', grams: ['nomn'] })).toEqual(true);
29
+ expect(wordFormEquals({ text: '', grams: ['nomn'] }, { text: '', grams: ['sing'] })).toEqual(false);
30
+ expect(wordFormEquals({ text: '', grams: ['nomn', 'sing'] }, { text: '', grams: ['nomn', 'sing'] })).toEqual(true);
31
+ expect(wordFormEquals({ text: '', grams: ['nomn', 'sing'] }, { text: '11', grams: ['nomn', 'sing'] })).toEqual(
32
+ true
33
+ );
34
+ expect(wordFormEquals({ text: '11', grams: ['nomn', 'sing'] }, { text: '', grams: ['nomn', 'sing'] })).toEqual(
35
+ true
36
+ );
37
+ expect(wordFormEquals({ text: '11', grams: ['nomn', 'sing'] }, { text: '11', grams: ['nomn', 'sing'] })).toEqual(
38
+ true
39
+ );
40
+ expect(wordFormEquals({ text: '42', grams: ['nomn', 'sing'] }, { text: '11', grams: ['nomn', 'sing'] })).toEqual(
41
+ true
42
+ );
43
+ expect(wordFormEquals({ text: '', grams: ['nomn', 'sing'] }, { text: '', grams: ['sing', 'nomn'] })).toEqual(false);
44
+ expect(wordFormEquals({ text: '', grams: ['nomn', 'sing'] }, { text: '', grams: ['nomn'] })).toEqual(false);
45
+ expect(wordFormEquals({ text: '', grams: ['nomn', 'nomn'] }, { text: '', grams: ['nomn'] })).toEqual(false);
46
+ });
47
+ });
48
+
49
+ describe('Testing grammeme parsing', () => {
50
+ it('empty input', () => {
51
+ expect(parseGrammemes('')).toStrictEqual([]);
52
+ expect(parseGrammemes(' ')).toStrictEqual([]);
53
+ expect(parseGrammemes(' , ')).toStrictEqual([]);
54
+ });
55
+
56
+ it('regular grammemes', () => {
57
+ expect(parseGrammemes('sing,nomn')).toStrictEqual([Grammeme.sing, Grammeme.nomn]);
58
+ expect(parseGrammemes('nomn,sing')).toStrictEqual([Grammeme.sing, Grammeme.nomn]);
59
+ });
60
+
61
+ it('custom grammemes', () => {
62
+ expect(parseGrammemes('nomn,invalid,sing')).toStrictEqual([Grammeme.sing, Grammeme.nomn, 'invalid']);
63
+ expect(parseGrammemes('invalid,test')).toStrictEqual(['invalid', 'test']);
64
+ });
65
+ });
66
+
67
+ describe('Testing nominal lexeme generation', () => {
68
+ it('generates every supported noun form with nominal text', () => {
69
+ const response = generateNominalLexeme({ text: 'nominal concept' });
70
+
71
+ expect(response.items).toStrictEqual([
72
+ { text: 'nominal concept', grams: 'sing,nomn' },
73
+ { text: 'nominal concept', grams: 'sing,gent' },
74
+ { text: 'nominal concept', grams: 'sing,datv' },
75
+ { text: 'nominal concept', grams: 'sing,accs' },
76
+ { text: 'nominal concept', grams: 'sing,ablt' },
77
+ { text: 'nominal concept', grams: 'sing,loct' },
78
+ { text: 'nominal concept', grams: 'plur,nomn' },
79
+ { text: 'nominal concept', grams: 'plur,gent' },
80
+ { text: 'nominal concept', grams: 'plur,datv' },
81
+ { text: 'nominal concept', grams: 'plur,accs' },
82
+ { text: 'nominal concept', grams: 'plur,ablt' },
83
+ { text: 'nominal concept', grams: 'plur,loct' }
84
+ ]);
85
+ });
86
+ });
87
+
88
+ describe('Testing reference parsing', () => {
89
+ it('entity reference', () => {
90
+ expect(parseEntityReference('@{ X1 | nomn,sing }')).toStrictEqual({ entity: 'X1', tags: ['sing', 'nomn'] });
91
+ expect(parseEntityReference('@{X1|nomn,sing}')).toStrictEqual({ entity: 'X1', tags: ['sing', 'nomn'] });
92
+ expect(parseEntityReference('@{X111|nomn,sing}')).toStrictEqual({ entity: 'X111', tags: ['sing', 'nomn'] });
93
+ });
94
+
95
+ it('syntactic reference', () => {
96
+ expect(parseSyntacticReference('@{1|test test}')).toStrictEqual({ offset: 1, nominal: 'test test' });
97
+ expect(parseSyntacticReference('@{101|test test}')).toStrictEqual({ offset: 101, nominal: 'test test' });
98
+ expect(parseSyntacticReference('@{-1|test test}')).toStrictEqual({ offset: -1, nominal: 'test test' });
99
+ expect(parseSyntacticReference('@{-99|test test}')).toStrictEqual({ offset: -99, nominal: 'test test' });
100
+ });
101
+
102
+ it('validated reference', () => {
103
+ expect(parseReference('@{X1|nomn, sing}')).toStrictEqual({
104
+ type: 'entity',
105
+ data: { entity: 'X1', tags: ['sing', 'nomn'] }
106
+ });
107
+ expect(parseReference('@{-1|derived term}')).toStrictEqual({
108
+ type: 'syntax',
109
+ data: { offset: -1, nominal: 'derived term' }
110
+ });
111
+ expect(parseReference('@{0|derived term}')).toBeNull();
112
+ expect(parseReference('@{-0|derived term}')).toBeNull();
113
+ expect(parseReference('@{1|}')).toBeNull();
114
+ });
115
+ });
116
+
117
+ describe('Testing reference resolution', () => {
118
+ it('extracts entity references', () => {
119
+ expect(extractEntities('@{X1|} and @{1|nominal} and @{X2|sing,gent} and @{X1|plur,nomn}')).toStrictEqual([
120
+ 'X1',
121
+ 'X2'
122
+ ]);
123
+ });
124
+
125
+ it('resolves entity references using nominal and manual forms', () => {
126
+ const context = {
127
+ X1: {
128
+ nominal: 'base term',
129
+ forms: [{ text: 'manual plural', grams: [Grammeme.plur, Grammeme.nomn] }]
130
+ }
131
+ };
132
+
133
+ expect(resolveTextReferences('Use @{X1|nomn,sing}.', context)).toBe('Use base term.');
134
+ expect(resolveTextReferences('Use @{X1|plur,nomn}.', context)).toBe('Use manual plural.');
135
+ expect(resolveTextReferences('Use @{X1|sing,gent}.', context)).toBe('Use base term.');
136
+ });
137
+
138
+ it('resolves syntactic references to nominal text without inflection', () => {
139
+ const context = {
140
+ X1: {
141
+ nominal: 'base term'
142
+ }
143
+ };
144
+
145
+ expect(resolveTextReferences('@{X1|nomn,sing} @{-1|derived term}', context)).toBe('base term derived term');
146
+ expect(resolveTextReferences('@{1|derived term} @{X1|nomn,sing}', context)).toBe('derived term base term');
147
+ expect(resolveTextReferences('@{-1|derived term}', context)).toBe('!Некорректное смещение: -1!');
148
+ });
149
+ });
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Module: Natural language model API.
3
+ */
4
+
5
+ import {
6
+ Case,
7
+ type EntityReference,
8
+ Grammeme,
9
+ type IReference,
10
+ Plurality,
11
+ ReferenceType,
12
+ type SyntacticReference,
13
+ type TermContext,
14
+ type TermContextItem,
15
+ type WordForm
16
+ } from './language';
17
+
18
+ interface Position {
19
+ from: number;
20
+ to: number;
21
+ }
22
+
23
+ interface ResolvedReference {
24
+ ref: IReference;
25
+ resolved: string;
26
+ posInput: Position;
27
+ }
28
+
29
+ const REFERENCE_PATTERN = /@{[^{}]*?}/g;
30
+ const ENTITY_REFERENCE_PATTERN = /@{([^0-9\-][^}|{]*?)\|([^}|{]*?)}/g;
31
+
32
+ /** Represents generated lexeme forms. */
33
+ export interface Lexeme {
34
+ items: { text: string; grams: string }[];
35
+ }
36
+
37
+ /** Equality comparator for {@link WordForm}. Compares a set of Grammemes attached to wordforms. */
38
+ export function wordFormEquals(left: WordForm, right: WordForm): boolean {
39
+ if (left.grams.length !== right.grams.length) {
40
+ return false;
41
+ }
42
+ for (let index = 0; index < left.grams.length; ++index) {
43
+ if (left.grams[index] !== right.grams[index]) {
44
+ return false;
45
+ }
46
+ }
47
+ return true;
48
+ }
49
+
50
+ /** Transforms {@link Grammeme} enumeration to {@link Grammeme}. */
51
+ export function parseGrammemes(termForm: string): Grammeme[] {
52
+ const result: Grammeme[] = [];
53
+ for (const chunk of termForm.split(',')) {
54
+ const gram = chunk.trim();
55
+ if (gram !== '') {
56
+ result.push(gram as Grammeme);
57
+ }
58
+ }
59
+ return result.sort(grammemeCompare);
60
+ }
61
+
62
+ /** Generates all supported noun forms using nominal text for every form. */
63
+ export function generateNominalLexeme(data: { text: string }): Lexeme {
64
+ return {
65
+ items: Plurality.flatMap(plurality =>
66
+ Case.map(gramCase => ({
67
+ text: data.text,
68
+ grams: `${plurality},${gramCase}`
69
+ }))
70
+ )
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Extracts {@link EntityReference} from string representation.
76
+ *
77
+ * @param text - Reference text in a valid pattern. Must fit format '\@\{GLOBAL_ID|GRAMMEMES\}'
78
+ */
79
+ export function parseEntityReference(text: string): EntityReference {
80
+ const ref = parseReference(text);
81
+ if (ref?.type !== ReferenceType.ENTITY) {
82
+ throw new Error(`Invalid entity reference: ${text}`);
83
+ }
84
+ return ref.data as EntityReference;
85
+ }
86
+
87
+ /**
88
+ * Extracts {@link SyntacticReference} from string representation.
89
+ *
90
+ * @param text - Reference text in a valid pattern. Must fit format '\@\{OFFSET|NOMINAL_FORM\}'
91
+ */
92
+ export function parseSyntacticReference(text: string): SyntacticReference {
93
+ const ref = parseReference(text);
94
+ if (ref?.type !== ReferenceType.SYNTACTIC) {
95
+ throw new Error(`Invalid syntactic reference: ${text}`);
96
+ }
97
+ return ref.data as SyntacticReference;
98
+ }
99
+
100
+ /** Extracts a validated reference from string representation. */
101
+ export function parseReference(text: string): IReference | null {
102
+ if (text.length < 4 || !text.startsWith('@{') || !text.endsWith('}')) {
103
+ return null;
104
+ }
105
+
106
+ const blocks = text
107
+ .slice(2, text.length - 1)
108
+ .split('|')
109
+ .map(block => block.trim());
110
+ if (blocks.length !== 2 || blocks[0] === '' || blocks[0].startsWith('0')) {
111
+ return null;
112
+ }
113
+
114
+ if (blocks[0].startsWith('-') || /^[1-9]/.test(blocks[0])) {
115
+ const offset = Number(blocks[0]);
116
+ if (!Number.isInteger(offset) || offset === 0 || blocks[1] === '') {
117
+ return null;
118
+ }
119
+ return {
120
+ type: ReferenceType.SYNTACTIC,
121
+ data: { offset, nominal: blocks[1] }
122
+ };
123
+ }
124
+
125
+ return {
126
+ type: ReferenceType.ENTITY,
127
+ data: { entity: blocks[0], tags: parseGrammemes(blocks[1].replaceAll(' ', '')) }
128
+ };
129
+ }
130
+
131
+ /** Extracts unique entity aliases referenced by text. */
132
+ export function extractEntities(text: string): string[] {
133
+ const result: string[] = [];
134
+ for (const segment of text.matchAll(ENTITY_REFERENCE_PATTERN)) {
135
+ const entity = segment[1].trim();
136
+ if (!result.includes(entity)) {
137
+ result.push(entity);
138
+ }
139
+ }
140
+ return result;
141
+ }
142
+
143
+ /** Resolves text references using nominal terms and optional manually edited forms. */
144
+ export function resolveTextReferences(text: string, context: TermContext): string {
145
+ const references = parseReferences(text);
146
+ if (references.length === 0) {
147
+ return text;
148
+ }
149
+
150
+ for (const ref of references) {
151
+ if (ref.ref.type === ReferenceType.ENTITY) {
152
+ ref.resolved = resolveEntity(ref.ref.data as EntityReference, context);
153
+ }
154
+ }
155
+ references.forEach((ref, index) => {
156
+ if (ref.ref.type === ReferenceType.SYNTACTIC) {
157
+ ref.resolved = resolveSyntactic(ref.ref.data as SyntacticReference, index, references);
158
+ }
159
+ });
160
+
161
+ let posInput = 0;
162
+ let output = '';
163
+ for (const ref of references) {
164
+ output += text.substring(posInput, ref.posInput.from);
165
+ output += ref.resolved;
166
+ posInput = ref.posInput.to;
167
+ }
168
+ output += text.substring(posInput);
169
+ return output;
170
+ }
171
+
172
+ /** Transforms {@link IReference} to string representation. */
173
+ export function referenceToString(ref: IReference): string {
174
+ switch (ref.type) {
175
+ case ReferenceType.ENTITY: {
176
+ const entity = ref.data as EntityReference;
177
+ return `@{${entity.entity}|${entity.tags.join(',')}}`;
178
+ }
179
+ case ReferenceType.SYNTACTIC: {
180
+ const syntactic = ref.data as SyntacticReference;
181
+ return `@{${syntactic.offset}|${syntactic.nominal}}`;
182
+ }
183
+ }
184
+ }
185
+
186
+ // ===== Internals =======
187
+
188
+ /** Compares {@link Grammeme} based on Grammeme enum and alpha order for strings. */
189
+ function grammemeCompare(left: Grammeme, right: Grammeme): number {
190
+ const indexLeft = Object.values(Grammeme).findIndex(gram => gram === left);
191
+ const indexRight = Object.values(Grammeme).findIndex(gram => gram === right);
192
+ if (indexLeft === -1 && indexRight === -1) {
193
+ return left.localeCompare(right);
194
+ } else if (indexLeft === -1 && indexRight !== -1) {
195
+ return 1;
196
+ } else if (indexLeft !== -1 && indexRight === -1) {
197
+ return -1;
198
+ } else {
199
+ return indexLeft - indexRight;
200
+ }
201
+ }
202
+
203
+ function parseReferences(text: string): ResolvedReference[] {
204
+ const result: ResolvedReference[] = [];
205
+ for (const segment of text.matchAll(REFERENCE_PATTERN)) {
206
+ const ref = parseReference(segment[0]);
207
+ if (ref) {
208
+ result.push({
209
+ ref,
210
+ resolved: '',
211
+ posInput: { from: segment.index ?? 0, to: (segment.index ?? 0) + segment[0].length }
212
+ });
213
+ }
214
+ }
215
+ return result;
216
+ }
217
+
218
+ function resolveEntity(ref: EntityReference, context: TermContext): string {
219
+ const entity = context[ref.entity];
220
+ if (!entity) {
221
+ return `!Неизвестная сущность: ${ref.entity}!`;
222
+ }
223
+
224
+ const resolved = getEntityForm(entity, ref.tags);
225
+ return resolved === '' ? `!Отсутствует термин: ${ref.entity}!` : resolved;
226
+ }
227
+
228
+ function getEntityForm(entity: TermContextItem, grams: Grammeme[]): string {
229
+ if (grams.length === 0) {
230
+ return entity.nominal;
231
+ }
232
+
233
+ const manual = entity.forms?.find(form => matchGrams(grams, form.grams));
234
+ return manual?.text ?? entity.nominal;
235
+ }
236
+
237
+ function matchGrams(query: Grammeme[], candidate: Grammeme[]): boolean {
238
+ for (const gram of candidate) {
239
+ if (!query.includes(gram)) {
240
+ return false;
241
+ }
242
+ }
243
+ return true;
244
+ }
245
+
246
+ function resolveSyntactic(ref: SyntacticReference, index: number, references: ResolvedReference[]): string {
247
+ const master = findSyntacticMaster(ref.offset, index, references);
248
+ if (!master) {
249
+ return `!Некорректное смещение: ${ref.offset}!`;
250
+ }
251
+ return ref.nominal;
252
+ }
253
+
254
+ function findSyntacticMaster(
255
+ offset: number,
256
+ index: number,
257
+ references: ResolvedReference[]
258
+ ): ResolvedReference | undefined {
259
+ if (offset > 0) {
260
+ let position = index + 1;
261
+ let left = offset;
262
+ while (position < references.length) {
263
+ if (references[position].ref.type === ReferenceType.ENTITY) {
264
+ if (left === 1) {
265
+ return references[position];
266
+ }
267
+ left -= 1;
268
+ }
269
+ position += 1;
270
+ }
271
+ } else {
272
+ let position = index - 1;
273
+ let left = offset;
274
+ while (position >= 0) {
275
+ if (references[position].ref.type === ReferenceType.ENTITY) {
276
+ if (left === -1) {
277
+ return references[position];
278
+ }
279
+ left += 1;
280
+ }
281
+ position -= 1;
282
+ }
283
+ }
284
+ return undefined;
285
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Module: Natural language model declarations.
3
+ */
4
+
5
+ /** Represents single unit of language Morphology. */
6
+ // prettier-ignore
7
+ export const Grammeme = {
8
+ // Число
9
+ sing: 'sing', plur: 'plur',
10
+
11
+ // Падеж
12
+ nomn: 'nomn', gent: 'gent', datv: 'datv',
13
+ accs: 'accs', ablt: 'ablt', loct: 'loct',
14
+ } as const;
15
+ export type Grammeme = (typeof Grammeme)[keyof typeof Grammeme];
16
+
17
+ /** Represents case language concept. */
18
+ export const Case: Grammeme[] = [
19
+ Grammeme.nomn,
20
+ Grammeme.gent,
21
+ Grammeme.datv,
22
+ Grammeme.accs,
23
+ Grammeme.ablt,
24
+ Grammeme.loct
25
+ ] as const;
26
+
27
+ /** Represents plurality language concept. */
28
+ export const Plurality: Grammeme[] = [Grammeme.sing, Grammeme.plur] as const;
29
+
30
+ /** Represents specific wordform attached to {@link Grammeme}s. */
31
+ export interface WordForm {
32
+ text: string;
33
+ grams: Grammeme[];
34
+ }
35
+
36
+ /** Represents a term available for text reference resolution. */
37
+ export interface TermContextItem {
38
+ nominal: string;
39
+ forms?: WordForm[];
40
+ }
41
+
42
+ /** Represents term lookup context keyed by entity alias. */
43
+ export type TermContext = Record<string, TermContextItem>;
44
+
45
+ /**
46
+ * Represents list of {@link Grammeme}s available in reference construction.
47
+ */
48
+ // prettier-ignore
49
+ export const supportedGrammemes = [
50
+ Grammeme.sing, Grammeme.plur,
51
+ Grammeme.nomn, Grammeme.gent, Grammeme.datv,
52
+ Grammeme.accs, Grammeme.ablt, Grammeme.loct,
53
+ ] as const;
54
+
55
+ // ====== Reference resolution =====
56
+
57
+ /** Represents text reference type. */
58
+ export const ReferenceType = {
59
+ ENTITY: 'entity',
60
+ SYNTACTIC: 'syntax'
61
+ } as const;
62
+ export type ReferenceType = (typeof ReferenceType)[keyof typeof ReferenceType];
63
+
64
+ /** Represents entity reference payload. */
65
+ export interface EntityReference {
66
+ entity: string;
67
+ tags: Grammeme[];
68
+ }
69
+
70
+ /** Represents syntactic reference payload. */
71
+ export interface SyntacticReference {
72
+ offset: number;
73
+ nominal: string;
74
+ }
75
+
76
+ /** Represents abstract reference data. */
77
+ export interface IReference {
78
+ type: ReferenceType;
79
+ data: EntityReference | SyntacticReference;
80
+ }