typengine 0.0.2
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 +34 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +492 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Shuhei Akutagawa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Typengine
|
|
2
|
+
|
|
3
|
+
Core library for building typing apps/games on any JavaScript runtime.
|
|
4
|
+
|
|
5
|
+
## Usage (draft)
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Sentence, createJapaneseSentenceDefinition } from "typengine";
|
|
9
|
+
|
|
10
|
+
const definition = createJapaneseSentenceDefinition("寿司", "すし");
|
|
11
|
+
const sentence = new Sentence(definition);
|
|
12
|
+
|
|
13
|
+
sentence.input("s");
|
|
14
|
+
sentence.input("u");
|
|
15
|
+
sentence.input("s");
|
|
16
|
+
const result = sentence.input("i");
|
|
17
|
+
// result example:
|
|
18
|
+
// {
|
|
19
|
+
// accepted: boolean; // whether this keystroke was accepted
|
|
20
|
+
// }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { Session, Sentence, createJapaneseSentenceDefinition } from "typengine";
|
|
25
|
+
|
|
26
|
+
const sentences = [
|
|
27
|
+
new Sentence(createJapaneseSentenceDefinition("寿司", "すし")),
|
|
28
|
+
new Sentence(createJapaneseSentenceDefinition("天ぷら", "てんぷら")),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const session = new Session(sentences);
|
|
32
|
+
session.start();
|
|
33
|
+
session.input("s");
|
|
34
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
//#region lib/input.d.ts
|
|
2
|
+
|
|
3
|
+
type InputResult = {
|
|
4
|
+
accepted: boolean;
|
|
5
|
+
};
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region lib/types.d.ts
|
|
8
|
+
type CharacterDefinition = {
|
|
9
|
+
reading: string;
|
|
10
|
+
patterns: string[];
|
|
11
|
+
};
|
|
12
|
+
type SentenceDefinition = {
|
|
13
|
+
text: string;
|
|
14
|
+
reading: string;
|
|
15
|
+
characters: CharacterDefinition[];
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region lib/character.d.ts
|
|
19
|
+
type CharacterOptions = {
|
|
20
|
+
onCharacterStarted?: (payload: {
|
|
21
|
+
startedAt: number;
|
|
22
|
+
}) => void;
|
|
23
|
+
onCharacterTyped?: (payload: {
|
|
24
|
+
typedAt: number;
|
|
25
|
+
key: string;
|
|
26
|
+
}) => void;
|
|
27
|
+
onCharacterMistyped?: (payload: {
|
|
28
|
+
typedAt: number;
|
|
29
|
+
key: string;
|
|
30
|
+
}) => void;
|
|
31
|
+
onCharacterCompleted?: (payload: {
|
|
32
|
+
completedAt: number;
|
|
33
|
+
}) => void;
|
|
34
|
+
};
|
|
35
|
+
declare class Character {
|
|
36
|
+
readonly definition: CharacterDefinition;
|
|
37
|
+
private readonly options;
|
|
38
|
+
private state;
|
|
39
|
+
constructor(definition: CharacterDefinition, options?: CharacterOptions);
|
|
40
|
+
start(): void;
|
|
41
|
+
input(value: string): InputResult;
|
|
42
|
+
get typed(): string;
|
|
43
|
+
get remainingPatterns(): string[];
|
|
44
|
+
get previewPattern(): string;
|
|
45
|
+
get completed(): boolean;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region lib/sentence.d.ts
|
|
49
|
+
type SentenceOptions = {
|
|
50
|
+
onSentenceStarted?: (payload: {
|
|
51
|
+
startedAt: number;
|
|
52
|
+
}) => void;
|
|
53
|
+
onSentenceCompleted?: (payload: {
|
|
54
|
+
completedAt: number;
|
|
55
|
+
}) => void;
|
|
56
|
+
onCharacterStarted?: (payload: {
|
|
57
|
+
startedAt: number;
|
|
58
|
+
}) => void;
|
|
59
|
+
onCharacterTyped?: (payload: {
|
|
60
|
+
typedAt: number;
|
|
61
|
+
key: string;
|
|
62
|
+
}) => void;
|
|
63
|
+
onCharacterMistyped?: (payload: {
|
|
64
|
+
typedAt: number;
|
|
65
|
+
key: string;
|
|
66
|
+
}) => void;
|
|
67
|
+
onCharacterCompleted?: (payload: {
|
|
68
|
+
completedAt: number;
|
|
69
|
+
}) => void;
|
|
70
|
+
};
|
|
71
|
+
declare class Sentence {
|
|
72
|
+
readonly definition: SentenceDefinition;
|
|
73
|
+
private readonly characters;
|
|
74
|
+
private readonly options;
|
|
75
|
+
constructor(definition: SentenceDefinition, options?: SentenceOptions);
|
|
76
|
+
start(): void;
|
|
77
|
+
input(value: string): InputResult;
|
|
78
|
+
get typed(): string;
|
|
79
|
+
get currentCharacter(): Character | null;
|
|
80
|
+
get position(): number;
|
|
81
|
+
get completed(): boolean;
|
|
82
|
+
get previewPattern(): string;
|
|
83
|
+
get text(): string;
|
|
84
|
+
get reading(): string;
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region lib/session.d.ts
|
|
88
|
+
type SessionOptions = {
|
|
89
|
+
onSessionStarted?: (payload: {
|
|
90
|
+
startedAt: number;
|
|
91
|
+
}) => void;
|
|
92
|
+
onSessionCompleted?: (payload: {
|
|
93
|
+
completedAt: number;
|
|
94
|
+
}) => void;
|
|
95
|
+
};
|
|
96
|
+
type SessionInputResult = {
|
|
97
|
+
accepted: boolean;
|
|
98
|
+
};
|
|
99
|
+
declare class Session {
|
|
100
|
+
readonly sentences: Sentence[];
|
|
101
|
+
private readonly options;
|
|
102
|
+
constructor(sentences: Sentence[], options?: SessionOptions);
|
|
103
|
+
start(): void;
|
|
104
|
+
input(value: string): SessionInputResult;
|
|
105
|
+
get currentSentence(): Sentence | null;
|
|
106
|
+
get position(): number;
|
|
107
|
+
get completed(): boolean;
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region lib/createJapaneseSentenceDefinition.d.ts
|
|
111
|
+
declare const createJapaneseSentenceDefinition: (text: string, reading: string) => SentenceDefinition;
|
|
112
|
+
//#endregion
|
|
113
|
+
export { Character, type CharacterDefinition, type CharacterOptions, type InputResult, Sentence, type SentenceDefinition, type SentenceOptions, Session, type SessionInputResult, type SessionOptions, createJapaneseSentenceDefinition };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
//#region lib/input.ts
|
|
2
|
+
const input = (state, value) => {
|
|
3
|
+
if (state.remainingPatterns.some((pattern) => pattern.length === 0)) return { accepted: false };
|
|
4
|
+
const nextPatterns = state.remainingPatterns.filter((pattern) => pattern.startsWith(value)).map((pattern) => pattern.slice(value.length));
|
|
5
|
+
if (nextPatterns.length === 0) return { accepted: false };
|
|
6
|
+
state.remainingPatterns = nextPatterns;
|
|
7
|
+
state.typedValue += value;
|
|
8
|
+
return { accepted: true };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region lib/character.ts
|
|
13
|
+
var Character = class {
|
|
14
|
+
definition;
|
|
15
|
+
options;
|
|
16
|
+
state;
|
|
17
|
+
constructor(definition, options = {}) {
|
|
18
|
+
this.definition = definition;
|
|
19
|
+
this.options = options;
|
|
20
|
+
this.state = {
|
|
21
|
+
remainingPatterns: [...definition.patterns],
|
|
22
|
+
typedValue: ""
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
start() {
|
|
26
|
+
this.options.onCharacterStarted?.({ startedAt: Date.now() });
|
|
27
|
+
}
|
|
28
|
+
input(value) {
|
|
29
|
+
const result = input(this.state, value);
|
|
30
|
+
const typedAt = Date.now();
|
|
31
|
+
if (!result.accepted) {
|
|
32
|
+
this.options.onCharacterMistyped?.({
|
|
33
|
+
typedAt,
|
|
34
|
+
key: value
|
|
35
|
+
});
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
this.options.onCharacterTyped?.({
|
|
39
|
+
typedAt,
|
|
40
|
+
key: value
|
|
41
|
+
});
|
|
42
|
+
if (this.completed) this.options.onCharacterCompleted?.({ completedAt: typedAt });
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
get typed() {
|
|
46
|
+
return this.state.typedValue;
|
|
47
|
+
}
|
|
48
|
+
get remainingPatterns() {
|
|
49
|
+
return [...this.state.remainingPatterns];
|
|
50
|
+
}
|
|
51
|
+
get previewPattern() {
|
|
52
|
+
const [pattern] = this.state.remainingPatterns;
|
|
53
|
+
return `${this.state.typedValue}${pattern ?? ""}`;
|
|
54
|
+
}
|
|
55
|
+
get completed() {
|
|
56
|
+
return this.state.remainingPatterns.some((pattern) => pattern.length === 0);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region lib/sentence.ts
|
|
62
|
+
var Sentence = class {
|
|
63
|
+
definition;
|
|
64
|
+
characters;
|
|
65
|
+
options;
|
|
66
|
+
constructor(definition, options = {}) {
|
|
67
|
+
this.definition = definition;
|
|
68
|
+
this.options = options;
|
|
69
|
+
const characterOptions = {
|
|
70
|
+
onCharacterStarted: options.onCharacterStarted,
|
|
71
|
+
onCharacterTyped: options.onCharacterTyped,
|
|
72
|
+
onCharacterMistyped: options.onCharacterMistyped,
|
|
73
|
+
onCharacterCompleted: options.onCharacterCompleted
|
|
74
|
+
};
|
|
75
|
+
this.characters = definition.characters.map((character) => new Character(character, characterOptions));
|
|
76
|
+
}
|
|
77
|
+
start() {
|
|
78
|
+
const current = this.currentCharacter;
|
|
79
|
+
if (!current) throw new Error("Cannot start an empty sentence.");
|
|
80
|
+
this.options.onSentenceStarted?.({ startedAt: Date.now() });
|
|
81
|
+
current.start();
|
|
82
|
+
}
|
|
83
|
+
input(value) {
|
|
84
|
+
const current = this.currentCharacter;
|
|
85
|
+
if (!current) throw new Error("Cannot input to a completed sentence.");
|
|
86
|
+
const result = current.input(value);
|
|
87
|
+
if (!result.accepted) return result;
|
|
88
|
+
if (!current.completed) return result;
|
|
89
|
+
const next = this.currentCharacter;
|
|
90
|
+
if (next) {
|
|
91
|
+
next.start();
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
this.options.onSentenceCompleted?.({ completedAt: Date.now() });
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
get typed() {
|
|
98
|
+
return this.characters.map((character) => character.typed).join("");
|
|
99
|
+
}
|
|
100
|
+
get currentCharacter() {
|
|
101
|
+
return this.characters[this.position] ?? null;
|
|
102
|
+
}
|
|
103
|
+
get position() {
|
|
104
|
+
for (let index = 0; index < this.characters.length; index += 1) if (!this.characters[index].completed) return index;
|
|
105
|
+
return -1;
|
|
106
|
+
}
|
|
107
|
+
get completed() {
|
|
108
|
+
return this.position < 0;
|
|
109
|
+
}
|
|
110
|
+
get previewPattern() {
|
|
111
|
+
return this.characters.map((character) => character.previewPattern).join("");
|
|
112
|
+
}
|
|
113
|
+
get text() {
|
|
114
|
+
return this.definition.text;
|
|
115
|
+
}
|
|
116
|
+
get reading() {
|
|
117
|
+
return this.definition.reading;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region lib/session.ts
|
|
123
|
+
var Session = class {
|
|
124
|
+
sentences;
|
|
125
|
+
options;
|
|
126
|
+
constructor(sentences, options = {}) {
|
|
127
|
+
if (sentences.length === 0) throw new Error("Session requires at least one sentence.");
|
|
128
|
+
this.sentences = sentences;
|
|
129
|
+
this.options = options;
|
|
130
|
+
}
|
|
131
|
+
start() {
|
|
132
|
+
const current = this.currentSentence;
|
|
133
|
+
if (!current) throw new Error("Cannot start a completed session.");
|
|
134
|
+
this.options.onSessionStarted?.({ startedAt: Date.now() });
|
|
135
|
+
current.start();
|
|
136
|
+
}
|
|
137
|
+
input(value) {
|
|
138
|
+
const current = this.currentSentence;
|
|
139
|
+
if (!current) throw new Error("Cannot input to a completed session.");
|
|
140
|
+
const result = current.input(value);
|
|
141
|
+
if (!result.accepted) return result;
|
|
142
|
+
if (!current.completed) return result;
|
|
143
|
+
const next = this.currentSentence;
|
|
144
|
+
if (next) {
|
|
145
|
+
next.start();
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
this.options.onSessionCompleted?.({ completedAt: Date.now() });
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
get currentSentence() {
|
|
152
|
+
return this.sentences[this.position] ?? null;
|
|
153
|
+
}
|
|
154
|
+
get position() {
|
|
155
|
+
for (let index = 0; index < this.sentences.length; index += 1) if (!this.sentences[index].completed) return index;
|
|
156
|
+
return -1;
|
|
157
|
+
}
|
|
158
|
+
get completed() {
|
|
159
|
+
return this.position < 0;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region lib/createJapaneseSentenceDefinition.ts
|
|
165
|
+
const KANA_ROMAJI_MAP = {
|
|
166
|
+
あ: ["a"],
|
|
167
|
+
い: ["i"],
|
|
168
|
+
う: ["u"],
|
|
169
|
+
え: ["e"],
|
|
170
|
+
お: ["o"],
|
|
171
|
+
か: ["ka"],
|
|
172
|
+
き: ["ki"],
|
|
173
|
+
く: ["ku"],
|
|
174
|
+
け: ["ke"],
|
|
175
|
+
こ: ["ko"],
|
|
176
|
+
さ: ["sa"],
|
|
177
|
+
し: ["shi", "si"],
|
|
178
|
+
す: ["su"],
|
|
179
|
+
せ: ["se"],
|
|
180
|
+
そ: ["so"],
|
|
181
|
+
た: ["ta"],
|
|
182
|
+
ち: ["chi", "ti"],
|
|
183
|
+
つ: ["tsu", "tu"],
|
|
184
|
+
て: ["te"],
|
|
185
|
+
と: ["to"],
|
|
186
|
+
な: ["na"],
|
|
187
|
+
に: ["ni"],
|
|
188
|
+
ぬ: ["nu"],
|
|
189
|
+
ね: ["ne"],
|
|
190
|
+
の: ["no"],
|
|
191
|
+
は: ["ha"],
|
|
192
|
+
ひ: ["hi"],
|
|
193
|
+
ふ: ["fu", "hu"],
|
|
194
|
+
へ: ["he"],
|
|
195
|
+
ほ: ["ho"],
|
|
196
|
+
ま: ["ma"],
|
|
197
|
+
み: ["mi"],
|
|
198
|
+
む: ["mu"],
|
|
199
|
+
め: ["me"],
|
|
200
|
+
も: ["mo"],
|
|
201
|
+
や: ["ya"],
|
|
202
|
+
ゆ: ["yu"],
|
|
203
|
+
よ: ["yo"],
|
|
204
|
+
ら: ["ra"],
|
|
205
|
+
り: ["ri"],
|
|
206
|
+
る: ["ru"],
|
|
207
|
+
れ: ["re"],
|
|
208
|
+
ろ: ["ro"],
|
|
209
|
+
わ: ["wa"],
|
|
210
|
+
を: ["wo", "o"],
|
|
211
|
+
ん: ["n"],
|
|
212
|
+
が: ["ga"],
|
|
213
|
+
ぎ: ["gi"],
|
|
214
|
+
ぐ: ["gu"],
|
|
215
|
+
げ: ["ge"],
|
|
216
|
+
ご: ["go"],
|
|
217
|
+
ざ: ["za"],
|
|
218
|
+
じ: ["ji", "zi"],
|
|
219
|
+
ず: ["zu"],
|
|
220
|
+
ぜ: ["ze"],
|
|
221
|
+
ぞ: ["zo"],
|
|
222
|
+
だ: ["da"],
|
|
223
|
+
ぢ: ["ji", "di"],
|
|
224
|
+
づ: ["zu", "du"],
|
|
225
|
+
で: ["de"],
|
|
226
|
+
ど: ["do"],
|
|
227
|
+
ば: ["ba"],
|
|
228
|
+
び: ["bi"],
|
|
229
|
+
ぶ: ["bu"],
|
|
230
|
+
べ: ["be"],
|
|
231
|
+
ぼ: ["bo"],
|
|
232
|
+
ぱ: ["pa"],
|
|
233
|
+
ぴ: ["pi"],
|
|
234
|
+
ぷ: ["pu"],
|
|
235
|
+
ぺ: ["pe"],
|
|
236
|
+
ぽ: ["po"],
|
|
237
|
+
きゃ: [
|
|
238
|
+
"kya",
|
|
239
|
+
"kilya",
|
|
240
|
+
"kixya"
|
|
241
|
+
],
|
|
242
|
+
きゅ: [
|
|
243
|
+
"kyu",
|
|
244
|
+
"kilyu",
|
|
245
|
+
"kixyu"
|
|
246
|
+
],
|
|
247
|
+
きょ: [
|
|
248
|
+
"kyo",
|
|
249
|
+
"kilyo",
|
|
250
|
+
"kixyo"
|
|
251
|
+
],
|
|
252
|
+
ぎゃ: [
|
|
253
|
+
"gya",
|
|
254
|
+
"gilya",
|
|
255
|
+
"gixya"
|
|
256
|
+
],
|
|
257
|
+
ぎゅ: [
|
|
258
|
+
"gyu",
|
|
259
|
+
"gilyu",
|
|
260
|
+
"gixyu"
|
|
261
|
+
],
|
|
262
|
+
ぎょ: [
|
|
263
|
+
"gyo",
|
|
264
|
+
"gilyo",
|
|
265
|
+
"gixyo"
|
|
266
|
+
],
|
|
267
|
+
しゃ: [
|
|
268
|
+
"sha",
|
|
269
|
+
"sya",
|
|
270
|
+
"shilya",
|
|
271
|
+
"shixya",
|
|
272
|
+
"silya",
|
|
273
|
+
"sixya"
|
|
274
|
+
],
|
|
275
|
+
しゅ: [
|
|
276
|
+
"shu",
|
|
277
|
+
"syu",
|
|
278
|
+
"shilyu",
|
|
279
|
+
"shixyu",
|
|
280
|
+
"silyu",
|
|
281
|
+
"sixyu"
|
|
282
|
+
],
|
|
283
|
+
しょ: [
|
|
284
|
+
"sho",
|
|
285
|
+
"syo",
|
|
286
|
+
"shilyo",
|
|
287
|
+
"shixyo",
|
|
288
|
+
"silyo",
|
|
289
|
+
"sixyo"
|
|
290
|
+
],
|
|
291
|
+
じゃ: [
|
|
292
|
+
"ja",
|
|
293
|
+
"jya",
|
|
294
|
+
"zya",
|
|
295
|
+
"jilya",
|
|
296
|
+
"jixya",
|
|
297
|
+
"zilya",
|
|
298
|
+
"zixya"
|
|
299
|
+
],
|
|
300
|
+
じゅ: [
|
|
301
|
+
"ju",
|
|
302
|
+
"jyu",
|
|
303
|
+
"zyu",
|
|
304
|
+
"jilyu",
|
|
305
|
+
"jixyu",
|
|
306
|
+
"zilyu",
|
|
307
|
+
"zixyu"
|
|
308
|
+
],
|
|
309
|
+
じょ: [
|
|
310
|
+
"jo",
|
|
311
|
+
"jyo",
|
|
312
|
+
"zyo",
|
|
313
|
+
"jilyo",
|
|
314
|
+
"jixyo",
|
|
315
|
+
"zilyo",
|
|
316
|
+
"zixyo"
|
|
317
|
+
],
|
|
318
|
+
ちゃ: [
|
|
319
|
+
"cha",
|
|
320
|
+
"cya",
|
|
321
|
+
"tya",
|
|
322
|
+
"chilya",
|
|
323
|
+
"chixya",
|
|
324
|
+
"tilya",
|
|
325
|
+
"tixya"
|
|
326
|
+
],
|
|
327
|
+
ちゅ: [
|
|
328
|
+
"chu",
|
|
329
|
+
"cyu",
|
|
330
|
+
"tyu",
|
|
331
|
+
"chilyu",
|
|
332
|
+
"chixyu",
|
|
333
|
+
"tilyu",
|
|
334
|
+
"tixyu"
|
|
335
|
+
],
|
|
336
|
+
ちょ: [
|
|
337
|
+
"cho",
|
|
338
|
+
"cyo",
|
|
339
|
+
"tyo",
|
|
340
|
+
"chilyo",
|
|
341
|
+
"chixyo",
|
|
342
|
+
"tilyo",
|
|
343
|
+
"tixyo"
|
|
344
|
+
],
|
|
345
|
+
にゃ: [
|
|
346
|
+
"nya",
|
|
347
|
+
"nilya",
|
|
348
|
+
"nixya"
|
|
349
|
+
],
|
|
350
|
+
にゅ: [
|
|
351
|
+
"nyu",
|
|
352
|
+
"nilyu",
|
|
353
|
+
"nixyu"
|
|
354
|
+
],
|
|
355
|
+
にょ: [
|
|
356
|
+
"nyo",
|
|
357
|
+
"nilyo",
|
|
358
|
+
"nixyo"
|
|
359
|
+
],
|
|
360
|
+
ひゃ: [
|
|
361
|
+
"hya",
|
|
362
|
+
"hilya",
|
|
363
|
+
"hixya"
|
|
364
|
+
],
|
|
365
|
+
ひゅ: [
|
|
366
|
+
"hyu",
|
|
367
|
+
"hilyu",
|
|
368
|
+
"hixyu"
|
|
369
|
+
],
|
|
370
|
+
ひょ: [
|
|
371
|
+
"hyo",
|
|
372
|
+
"hilyo",
|
|
373
|
+
"hixyo"
|
|
374
|
+
],
|
|
375
|
+
みゃ: [
|
|
376
|
+
"mya",
|
|
377
|
+
"milya",
|
|
378
|
+
"mixya"
|
|
379
|
+
],
|
|
380
|
+
みゅ: [
|
|
381
|
+
"myu",
|
|
382
|
+
"milyu",
|
|
383
|
+
"mixyu"
|
|
384
|
+
],
|
|
385
|
+
みょ: [
|
|
386
|
+
"myo",
|
|
387
|
+
"milyo",
|
|
388
|
+
"mixyo"
|
|
389
|
+
],
|
|
390
|
+
りゃ: [
|
|
391
|
+
"rya",
|
|
392
|
+
"rilya",
|
|
393
|
+
"rixya"
|
|
394
|
+
],
|
|
395
|
+
りゅ: [
|
|
396
|
+
"ryu",
|
|
397
|
+
"rilyu",
|
|
398
|
+
"rixyu"
|
|
399
|
+
],
|
|
400
|
+
りょ: [
|
|
401
|
+
"ryo",
|
|
402
|
+
"rilyo",
|
|
403
|
+
"rixyo"
|
|
404
|
+
],
|
|
405
|
+
びゃ: [
|
|
406
|
+
"bya",
|
|
407
|
+
"bilya",
|
|
408
|
+
"bixya"
|
|
409
|
+
],
|
|
410
|
+
びゅ: [
|
|
411
|
+
"byu",
|
|
412
|
+
"bilyu",
|
|
413
|
+
"bixyu"
|
|
414
|
+
],
|
|
415
|
+
びょ: [
|
|
416
|
+
"byo",
|
|
417
|
+
"bilyo",
|
|
418
|
+
"bixyo"
|
|
419
|
+
],
|
|
420
|
+
ぴゃ: [
|
|
421
|
+
"pya",
|
|
422
|
+
"pilya",
|
|
423
|
+
"pixya"
|
|
424
|
+
],
|
|
425
|
+
ぴゅ: [
|
|
426
|
+
"pyu",
|
|
427
|
+
"pilyu",
|
|
428
|
+
"pixyu"
|
|
429
|
+
],
|
|
430
|
+
ぴょ: [
|
|
431
|
+
"pyo",
|
|
432
|
+
"pilyo",
|
|
433
|
+
"pixyo"
|
|
434
|
+
]
|
|
435
|
+
};
|
|
436
|
+
const SMALL_TSU_PATTERNS = [
|
|
437
|
+
"ltu",
|
|
438
|
+
"ltsu",
|
|
439
|
+
"xtu",
|
|
440
|
+
"xtsu"
|
|
441
|
+
];
|
|
442
|
+
const SMALL_TSU_CONSONANTS = new Set([
|
|
443
|
+
"k",
|
|
444
|
+
"s",
|
|
445
|
+
"t",
|
|
446
|
+
"h",
|
|
447
|
+
"m",
|
|
448
|
+
"y",
|
|
449
|
+
"r",
|
|
450
|
+
"w"
|
|
451
|
+
]);
|
|
452
|
+
const createSmallTsuCharacterDefinition = (nextPatterns) => {
|
|
453
|
+
const consonantPatterns = /* @__PURE__ */ new Set();
|
|
454
|
+
if (nextPatterns) for (const pattern of nextPatterns) {
|
|
455
|
+
const consonant = pattern[0];
|
|
456
|
+
if (consonant && SMALL_TSU_CONSONANTS.has(consonant)) consonantPatterns.add(consonant);
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
reading: "っ",
|
|
460
|
+
patterns: [...SMALL_TSU_PATTERNS, ...consonantPatterns]
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
const createJapaneseSentenceDefinition = (text, reading) => {
|
|
464
|
+
const characters = [];
|
|
465
|
+
for (let index = 0; index < reading.length; index += 1) {
|
|
466
|
+
const char = reading[index];
|
|
467
|
+
const nextChar = reading[index + 1];
|
|
468
|
+
if (char === "っ") {
|
|
469
|
+
const nextPatterns = nextChar ? KANA_ROMAJI_MAP[nextChar] : void 0;
|
|
470
|
+
characters.push(createSmallTsuCharacterDefinition(nextPatterns));
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const digraph = nextChar ? `${char}${nextChar}` : null;
|
|
474
|
+
const digraphCandidates = digraph ? KANA_ROMAJI_MAP[digraph] : void 0;
|
|
475
|
+
const candidates = digraphCandidates ?? KANA_ROMAJI_MAP[char];
|
|
476
|
+
if (!candidates) throw new Error(`Unsupported hiragana: ${char}`);
|
|
477
|
+
const readingUnit = digraphCandidates && nextChar ? `${char}${nextChar}` : char;
|
|
478
|
+
if (digraphCandidates) index += 1;
|
|
479
|
+
characters.push({
|
|
480
|
+
reading: readingUnit,
|
|
481
|
+
patterns: candidates
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
text,
|
|
486
|
+
reading,
|
|
487
|
+
characters
|
|
488
|
+
};
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
//#endregion
|
|
492
|
+
export { Character, Sentence, Session, createJapaneseSentenceDefinition };
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "typengine",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Core library for building typing apps and games.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"japanese",
|
|
7
|
+
"keyboard",
|
|
8
|
+
"romaji",
|
|
9
|
+
"typing",
|
|
10
|
+
"typing-game",
|
|
11
|
+
"typing-practice"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^25.0.3",
|
|
30
|
+
"@typescript/native-preview": "7.0.0-dev.20251226.1",
|
|
31
|
+
"oxfmt": "^0.20.0",
|
|
32
|
+
"oxlint": "^1.35.0",
|
|
33
|
+
"tsdown": "^0.15.12",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsdown",
|
|
38
|
+
"test": "node --test lib/**/*.test.ts",
|
|
39
|
+
"typecheck": "tsgo --noEmit",
|
|
40
|
+
"lint": "oxlint . && oxfmt --check .",
|
|
41
|
+
"fix": "oxlint . --fix && oxfmt --write .",
|
|
42
|
+
"ready": "pnpm fix && pnpm typecheck && pnpm test",
|
|
43
|
+
"ready:check": "pnpm lint && pnpm typecheck && pnpm test"
|
|
44
|
+
}
|
|
45
|
+
}
|