@effitor/assist-ai 0.2.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/README.md ADDED
@@ -0,0 +1 @@
1
+ # @effitor/assist-ai
package/dist/index.css ADDED
@@ -0,0 +1,35 @@
1
+
2
+ #Et_ai-deco {
3
+ display: none;
4
+ position: fixed;
5
+ top: 0;
6
+ left: 0;
7
+ min-height: 1em;
8
+ z-index: 9999;
9
+
10
+ &.Et_ai-mask {
11
+ width: 48px;
12
+ background-image: linear-gradient(to right, transparent, var(--et-color-bg));
13
+ transform: translateX(-36px);
14
+ }
15
+
16
+ &.Et_ai-caret {
17
+ /* 需给编辑器设置 caret-color: transparent 隐藏原生光标 */
18
+ &::after {
19
+ content: '';
20
+ position: absolute;
21
+ top: 0;
22
+ bottom: 0;
23
+ right: 0;
24
+ border-right: 1px solid;
25
+ filter: brightness(2);
26
+ animation: caret-blink 1s steps(1) infinite;
27
+ }
28
+ }
29
+ }
30
+
31
+ @keyframes caret-blink {
32
+ 50% {
33
+ opacity: 0;
34
+ }
35
+ }
@@ -0,0 +1,119 @@
1
+ import { Et } from '@effitor/core';
2
+
3
+ type MarkdownTextMapping = Record<string, MarkdownTextMappingFn> & {
4
+ beforeStart?: (ctx: Et.EditorContext) => null;
5
+ afterEnd?: (ctx: Et.EditorContext) => null;
6
+ };
7
+ interface MarkdownTextMappingFn {
8
+ /**
9
+ * 处理markdown特殊字符; 如连续的**, 要转为加粗, 而不是插入两个*
10
+ * @param index 当前字符在文本中的索引
11
+ * @param text 完整文本
12
+ * @returns 映射结果
13
+ */
14
+ (ctx: Et.EditorContext, index: number, text: string): MarkdownTextMappingFnResult;
15
+ }
16
+ /**
17
+ * 返回null, 无特殊处理\
18
+ * 返回{ key, data, nextIndex }, 表示将char替换为按下key或输入data, 并将索引设置到nextIndex;
19
+ * 返回(ctx: Et.EditorContext) => number, 表示自行处理, 返回下一个索引
20
+ */
21
+ type MarkdownTextMappingFnResult = null | ((ctx: Et.EditorContext) => number) | {
22
+ /** keydown的key值 或 beforeinput输入的data文本 */
23
+ value: string;
24
+ /** 映射类型, 表示value是keydown的key值 还是 beforeinput输入的data文本 */
25
+ type: 'key' | 'data';
26
+ /**
27
+ * 下一个输入的起始索引, 若该索引不是某个字素的首个字符索引, 则索引所在的整个字素会被跳过;
28
+ * 正常来讲, 要跳过`n`个字符(包括当前char) 就返回 `index + n` 即可
29
+ */
30
+ nextIndex: number;
31
+ };
32
+ interface CreateEffitorAIOptions {
33
+ /** 是否显示打字机效果的mask */
34
+ typingMask?: boolean;
35
+ /** 是否显示打字机效果的光标 */
36
+ typingCaret?: boolean;
37
+ /** 要添加的Markdown特殊字符的映射函数 */
38
+ markdownTextMappings?: MarkdownTextMapping[];
39
+ }
40
+
41
+ interface TypingResult {
42
+ /** 输入结果Promise, 完成时fulfilled, 失败时rejected */
43
+ finished: Promise<void>;
44
+ /** 暂停输入 */
45
+ pause: () => void;
46
+ /** 恢复输入 */
47
+ resume: () => void;
48
+ /** 取消后续输入 */
49
+ cancel: () => void;
50
+ }
51
+ type TypingMarkdownArray = readonly (readonly [number, number, boolean, string])[];
52
+ declare class EffitorAI {
53
+ private readonly _ctx;
54
+ private _markdownTextMapping;
55
+ private _useDeco;
56
+ private readonly _beforeCallbacks;
57
+ private readonly _afterCallbacks;
58
+ private readonly _maskEl;
59
+ constructor(_ctx: Et.EditorContext, options?: CreateEffitorAIOptions);
60
+ /**
61
+ * 添加Markdown特殊字符的映射函数; 当先添加的映射函数返回值非null时, 后续映射函数将不会被调用
62
+ * @param mapping 映射对象
63
+ */
64
+ addMarkdownTextMapping(mapping: MarkdownTextMapping): void;
65
+ /**
66
+ * 设置Markdown特殊字符的映射函数; 已存在会被覆盖
67
+ * @param char 特殊字符
68
+ * @param mappingFn 映射函数
69
+ */
70
+ setMarkdownTextMapping(char: string, mappingFn: MarkdownTextMappingFn): void;
71
+ setTypingDeco(typingCaret?: boolean, typingMask?: boolean): void;
72
+ /**
73
+ * 将Markdown内容依次输入到编辑器中
74
+ * * 目前此方法直接触发相关事件,依据当前光标位置插入内容;调用者需保证整个typing过程中,
75
+ * 编辑器内的光标不被意外的干扰,否则可能导致插入内容错误
76
+ * @param mdText Markdown文本;
77
+ * 在文本中,可用\x00引导一个按键,如:"\x00KeyZ,1000{21}"表示按下 Ctrl+Z 21 次;
78
+ * 修饰键用 4 个二进制位表示,顺序依次为:Ctrl, Shift, Alt, Meta
79
+ * @param byWord 是否按单词输入, 默认为true
80
+ * @param delay 每个字符之间的延迟时间(毫秒),最小值为30(默认 50ms)
81
+ */
82
+ typingMarkdown(mdText: string, byWord?: boolean, delay?: number): TypingResult;
83
+ /**
84
+ * 将Markdown内容依次输入到编辑器中
85
+ * * 目前此方法直接触发相关事件,依据当前光标位置插入内容;调用者需保证整个typing过程中,
86
+ * 编辑器内的光标不被意外的干扰,否则可能导致插入内容错误
87
+ * @param mdArray 输入数组, 每个元素为 [本轮结束后延迟, 每次输入延迟, 是否按单词输入, Markdown文本]
88
+ */
89
+ typingMarkdown(mdArray: TypingMarkdownArray): TypingResult;
90
+ private _genMdText;
91
+ private _startTyping;
92
+ private _stopTyping;
93
+ private _runBeforeCallbacks;
94
+ private _runAfterCallbacks;
95
+ private _startMask;
96
+ private _stopMask;
97
+ private _updateMask;
98
+ typeKey(init: KeyboardEventInit): void;
99
+ typeText(data: string): void;
100
+ }
101
+
102
+ declare const mappingForBlockquote: MarkdownTextMapping;
103
+
104
+ declare const mappingForCode: MarkdownTextMapping;
105
+
106
+ declare const mappingForList: MarkdownTextMapping;
107
+
108
+ declare const mappingForMark: MarkdownTextMapping;
109
+
110
+ declare module '@effitor/core' {
111
+ interface EditorAssists {
112
+ ai: EffitorAI;
113
+ }
114
+ }
115
+
116
+ type AIAssistOptions = {} & CreateEffitorAIOptions;
117
+ declare const useAIAssist: (options?: AIAssistOptions) => Et.EditorPlugin;
118
+
119
+ export { type AIAssistOptions, type TypingMarkdownArray, type TypingResult, mappingForBlockquote, mappingForCode, mappingForList, mappingForMark, useAIAssist };
package/dist/index.js ADDED
@@ -0,0 +1,698 @@
1
+ // src/index.ts
2
+ import "./index.css";
3
+
4
+ // src/EffitorAI.ts
5
+ import { reduceFnMappings } from "@effitor/core";
6
+
7
+ // src/mapping/builtin.ts
8
+ var builtinMapping = {
9
+ "\\": (_, i, s) => {
10
+ const n = s[i + 1];
11
+ if (n === "*" || n === "`" || n === "_" || n === "\\" || n === "=" || n === "~") {
12
+ return {
13
+ value: n,
14
+ type: "data",
15
+ nextIndex: i + 2
16
+ };
17
+ }
18
+ if (n === "\n") {
19
+ return (ctx) => {
20
+ ctx.body.dispatchInputEvent("beforeinput", { inputType: "insertLineBreak" });
21
+ return i + 2;
22
+ };
23
+ }
24
+ return null;
25
+ },
26
+ " ": (_, i, s) => {
27
+ if (s[i + 1] === " " && s[i + 2] === "\n") {
28
+ return (ctx) => {
29
+ ctx.body.dispatchInputEvent("beforeinput", { inputType: "insertLineBreak" });
30
+ return i + 2;
31
+ };
32
+ }
33
+ return null;
34
+ },
35
+ "\n": (ctx, i, s) => {
36
+ if (s[i + 1] === "\n" && ctx.isPlainParagraph(ctx.focusParagraph)) {
37
+ return (ctx2) => {
38
+ ctx2.body.dispatchInputEvent("beforeinput", { inputType: "insertParagraph" });
39
+ return i + 2;
40
+ };
41
+ }
42
+ return {
43
+ value: "Enter",
44
+ type: "key",
45
+ nextIndex: i + 1
46
+ };
47
+ },
48
+ " ": (_, i) => {
49
+ return {
50
+ value: "Tab",
51
+ type: "key",
52
+ nextIndex: i + 1
53
+ };
54
+ }
55
+ };
56
+
57
+ // src/EffitorAI.ts
58
+ var EffitorAI = class {
59
+ constructor(_ctx, options = {}) {
60
+ this._ctx = _ctx;
61
+ const mappings = [
62
+ ...options.markdownTextMappings || [],
63
+ builtinMapping
64
+ ].map((map) => {
65
+ const { beforeStart, afterEnd, ...rest } = map;
66
+ if (beforeStart) this._beforeCallbacks.push(beforeStart);
67
+ if (afterEnd) this._afterCallbacks.push(afterEnd);
68
+ return rest;
69
+ });
70
+ this._markdownTextMapping = reduceFnMappings(
71
+ mappings,
72
+ true
73
+ );
74
+ const mask = document.createElement("div");
75
+ mask.id = "Et_ai-deco" /* Id_Deco */;
76
+ if (options.typingCaret) {
77
+ this._useDeco = true;
78
+ mask.classList.add("Et_ai-caret" /* Class_Caret */);
79
+ }
80
+ if (options.typingMask) {
81
+ this._useDeco = true;
82
+ mask.classList.add("Et_ai-mask" /* Class_Mask */);
83
+ }
84
+ this._ctx.root.appendChild(mask);
85
+ this._maskEl = mask;
86
+ }
87
+ _markdownTextMapping;
88
+ _useDeco = false;
89
+ _beforeCallbacks = [];
90
+ _afterCallbacks = [];
91
+ _maskEl;
92
+ /**
93
+ * 添加Markdown特殊字符的映射函数; 当先添加的映射函数返回值非null时, 后续映射函数将不会被调用
94
+ * @param mapping 映射对象
95
+ */
96
+ addMarkdownTextMapping(mapping) {
97
+ if (!Object.keys(this._markdownTextMapping).length) {
98
+ Object.assign(this._markdownTextMapping, mapping);
99
+ return;
100
+ }
101
+ const { beforeStart, afterEnd, ...rest } = mapping;
102
+ if (beforeStart) this._beforeCallbacks.push(beforeStart);
103
+ if (afterEnd) this._afterCallbacks.push(afterEnd);
104
+ this._markdownTextMapping = reduceFnMappings([this._markdownTextMapping, rest], true);
105
+ }
106
+ /**
107
+ * 设置Markdown特殊字符的映射函数; 已存在会被覆盖
108
+ * @param char 特殊字符
109
+ * @param mappingFn 映射函数
110
+ */
111
+ setMarkdownTextMapping(char, mappingFn) {
112
+ this._markdownTextMapping[char] = mappingFn;
113
+ }
114
+ setTypingDeco(typingCaret = false, typingMask = false) {
115
+ this._useDeco = typingCaret || typingMask;
116
+ this._maskEl.classList.toggle("Et_ai-caret" /* Class_Caret */, typingCaret);
117
+ this._maskEl.classList.toggle("Et_ai-mask" /* Class_Mask */, typingMask);
118
+ }
119
+ typingMarkdown(mdTextOrArray, byWord = true, delay = 50) {
120
+ if (delay && delay < 30) {
121
+ delay = 30;
122
+ }
123
+ if (typeof mdTextOrArray === "string") {
124
+ mdTextOrArray = [[0, delay, byWord, mdTextOrArray]];
125
+ }
126
+ const state = {
127
+ canceled: Promise.resolve(false),
128
+ timer: 0,
129
+ pause: void 0,
130
+ resume: void 0,
131
+ doJob: void 0,
132
+ rejectJob: void 0,
133
+ cancelRun: void 0
134
+ };
135
+ const isReadonly = this._ctx.editor.status.readonly;
136
+ const run = async () => {
137
+ if (await state.canceled) return Promise.reject();
138
+ if (await state.canceled) return Promise.reject();
139
+ if (!isReadonly) {
140
+ this._ctx.editor.setReadonly(true);
141
+ }
142
+ this._startTyping();
143
+ for (const [perDelay, charDelay, byWord2, mdText] of mdTextOrArray) {
144
+ delay = charDelay;
145
+ const gen = this._genMdText(mdText, byWord2);
146
+ if (await state.canceled) return Promise.reject();
147
+ await new Promise((res, rej) => {
148
+ state.rejectJob = rej;
149
+ state.doJob = () => {
150
+ const timer = state.timer = window.setInterval(async () => {
151
+ if ((await gen.next()).done) {
152
+ res();
153
+ clearInterval(timer);
154
+ }
155
+ }, delay);
156
+ };
157
+ state.doJob();
158
+ });
159
+ await new Promise((res) => setTimeout(res, perDelay));
160
+ }
161
+ };
162
+ state.pause = () => {
163
+ const _pause = state.pause;
164
+ state.pause = void 0;
165
+ clearInterval(state.timer);
166
+ state.canceled = new Promise((res) => {
167
+ state.cancelRun = () => res(true);
168
+ state.resume = () => {
169
+ state.pause = _pause;
170
+ state.resume = void 0;
171
+ res(false);
172
+ state.doJob?.();
173
+ };
174
+ });
175
+ };
176
+ return {
177
+ finished: run().finally(() => {
178
+ if (!isReadonly) {
179
+ this._ctx.editor.setReadonly(false);
180
+ }
181
+ this._stopTyping();
182
+ }),
183
+ pause: () => state.pause?.(),
184
+ resume: () => state.resume?.(),
185
+ cancel: () => {
186
+ clearInterval(state.timer);
187
+ state.canceled = Promise.resolve(true);
188
+ state.pause = void 0;
189
+ state.resume = void 0;
190
+ state.cancelRun?.();
191
+ state.rejectJob?.();
192
+ }
193
+ };
194
+ }
195
+ async *_genMdText(mdText, byWord) {
196
+ const segItor = byWord ? this._ctx.segmenter.segmentWordItor(mdText) : this._ctx.segmenter.segmentGraphemeItor(mdText);
197
+ let nextIdx = 0;
198
+ const parseKey = async (start) => {
199
+ const j = mdText.indexOf(",", start);
200
+ if (j < 0) {
201
+ return start;
202
+ }
203
+ const code = mdText.slice(start, j);
204
+ const ctrlKey = mdText[j + 1] === "1";
205
+ const shiftKey = mdText[j + 2] === "1";
206
+ const altKey = mdText[j + 3] === "1";
207
+ const metaKey = mdText[j + 4] === "1";
208
+ const key = code === "Space" ? " " : code.startsWith("Key") ? shiftKey ? code.slice(3).toLowerCase() : code.slice(3) : code;
209
+ if (mdText[j + 5] === "{") {
210
+ const k = mdText.indexOf("}", j + 5);
211
+ const count = parseInt(mdText.slice(j + 6, k));
212
+ if (!isNaN(count)) {
213
+ return new Promise((res) => {
214
+ let i = 0;
215
+ const it = setInterval(() => {
216
+ this.typeKey({ key, code, ctrlKey, shiftKey, altKey, metaKey });
217
+ if (++i >= count) {
218
+ clearInterval(it);
219
+ res(k + 1);
220
+ }
221
+ }, 50);
222
+ });
223
+ }
224
+ }
225
+ this.typeKey({ key, code, ctrlKey, shiftKey, altKey, metaKey });
226
+ return j + 5;
227
+ };
228
+ const typeNext = async (segment, index) => {
229
+ if (segment === "\0" /* Typing_Key_Char */) {
230
+ nextIdx = await parseKey(index + 1);
231
+ return;
232
+ }
233
+ if (segment.length > 1 || segment.charCodeAt(0) > 127) {
234
+ return this.typeText(segment);
235
+ }
236
+ const ret = this._markdownTextMapping[segment]?.(this._ctx, index, mdText);
237
+ if (!ret) {
238
+ return this.typeKey({ key: segment, code: segment });
239
+ }
240
+ if (typeof ret === "function") {
241
+ nextIdx = ret(this._ctx);
242
+ return;
243
+ }
244
+ const { value, type, nextIndex } = ret;
245
+ nextIdx = nextIndex;
246
+ if (!value) {
247
+ return;
248
+ }
249
+ if (type === "key") {
250
+ this.typeKey({ key: value, code: value });
251
+ } else {
252
+ this.typeText(value);
253
+ }
254
+ };
255
+ let next;
256
+ while (!(next = segItor.next()).done) {
257
+ const { segment, index } = next.value;
258
+ if (!segment || nextIdx > index) {
259
+ continue;
260
+ }
261
+ if (!this._ctx.selection.range || !this._ctx.isUpdated()) {
262
+ this._ctx.focusToBodyEnd();
263
+ }
264
+ yield typeNext(segment, index);
265
+ }
266
+ }
267
+ _startTyping() {
268
+ this._ctx.editor.setReadonly(true);
269
+ this._ctx.bodyEl.style.pointerEvents = "none";
270
+ this._runBeforeCallbacks();
271
+ this._startMask();
272
+ }
273
+ _stopTyping() {
274
+ this._ctx.bodyEl.style.pointerEvents = "";
275
+ this._runAfterCallbacks();
276
+ this._stopMask();
277
+ }
278
+ _runBeforeCallbacks() {
279
+ this._beforeCallbacks.forEach((cb) => cb(this._ctx));
280
+ }
281
+ _runAfterCallbacks() {
282
+ this._afterCallbacks.forEach((cb) => cb(this._ctx));
283
+ }
284
+ _startMask() {
285
+ this._ctx.bodyEl.style.caretColor = "transparent";
286
+ }
287
+ _stopMask() {
288
+ this._ctx.bodyEl.style.caretColor = "";
289
+ this._maskEl.style.display = "none";
290
+ }
291
+ _updateMask() {
292
+ if (!this._useDeco) {
293
+ return;
294
+ }
295
+ if (this._ctx.selection.rawEl) {
296
+ this._maskEl.style.display = "none";
297
+ return;
298
+ }
299
+ const rect = this._ctx.selection.range?.getClientRects()[0];
300
+ if (rect) {
301
+ this._maskEl.style.height = `${rect.height}px`;
302
+ this._maskEl.style.translate = `${rect.left + 5}px ${rect.top}px`;
303
+ this._maskEl.style.display = "block";
304
+ }
305
+ }
306
+ typeKey(init) {
307
+ this._ctx.bodyEl.dispatchEvent(new KeyboardEvent("keydown", init));
308
+ this._ctx.bodyEl.dispatchEvent(new KeyboardEvent("keyup", init));
309
+ this._updateMask();
310
+ }
311
+ typeText(data) {
312
+ this._ctx.body.dispatchInputEvent("beforeinput", {
313
+ data,
314
+ inputType: "insertText"
315
+ });
316
+ this._updateMask();
317
+ }
318
+ };
319
+
320
+ // src/mapping/mappingForBlockquote.ts
321
+ import { dom } from "@effitor/core";
322
+ var blockquoteDepth = (ctx) => {
323
+ if (!ctx.focusParagraph) {
324
+ return 0;
325
+ }
326
+ let d = 0;
327
+ for (const p of ctx.body.outerParagraphs(ctx.focusParagraph)) {
328
+ if (p && ctx.schema.blockquote?.is(p) && !p.dataset.type?.startsWith("pg")) {
329
+ d++;
330
+ }
331
+ }
332
+ return d;
333
+ };
334
+ var checkInBlockquote = (ctx) => {
335
+ return ctx.schema.blockquote?.is(ctx.focusParagraph?.parentNode);
336
+ };
337
+ var mappingForBlockquote = {
338
+ ">": (ctx, i, s) => {
339
+ if (s[i + 1] !== " ") {
340
+ return null;
341
+ }
342
+ if (ctx.isPlainParagraph(ctx.focusParagraph) && dom.isEmptyContentNode(ctx.focusParagraph)) {
343
+ if (s[i + 2] === "[") {
344
+ return null;
345
+ }
346
+ const depth = blockquoteDepth(ctx);
347
+ if (!depth) {
348
+ return () => {
349
+ ctx.assists.ai.typeText(">");
350
+ ctx.assists.ai.typeKey({ key: "Enter", code: "Enter" });
351
+ return i + 2;
352
+ };
353
+ }
354
+ let k = i + 2;
355
+ for (let j = 1; j < depth; j++) {
356
+ if (s[k] === ">" && s[k + 1] === " ") {
357
+ if (s[k + 2] === "[") {
358
+ break;
359
+ }
360
+ k += 2;
361
+ continue;
362
+ }
363
+ break;
364
+ }
365
+ if (s[k] === ">" && s[k + 1] === " " && s[k + 2] !== "[") {
366
+ return () => {
367
+ ctx.assists.ai.typeText(">");
368
+ ctx.assists.ai.typeKey({ key: "Enter", code: "Enter" });
369
+ return i + 2;
370
+ };
371
+ }
372
+ return () => k;
373
+ }
374
+ return null;
375
+ },
376
+ "\n": (ctx, i, s) => {
377
+ if (s[i + 1] === ">") {
378
+ if (!checkInBlockquote(ctx)) {
379
+ return null;
380
+ }
381
+ return {
382
+ type: "key",
383
+ value: "Enter",
384
+ nextIndex: i + 2
385
+ };
386
+ }
387
+ if (!checkInBlockquote(ctx)) {
388
+ return null;
389
+ }
390
+ if (s[i + 1] === "\n" && i + 2 < s.length && s[i + 2] !== ">" && s[i + 2] !== "\n") {
391
+ return () => {
392
+ ctx.commonHandler.appendParagraph(null, { topLevel: true });
393
+ return i + 2;
394
+ };
395
+ }
396
+ return () => {
397
+ while (s[i] === "\n") {
398
+ i++;
399
+ ctx.assists.ai.typeKey({ key: "Enter", code: "Enter" });
400
+ }
401
+ return i;
402
+ };
403
+ }
404
+ };
405
+
406
+ // src/mapping/mappingForCode.ts
407
+ var checkInCodeEnd = (ctx, idx) => {
408
+ return ctx.focusEtElement?.localName === "et-code" ? (_ctx) => {
409
+ _ctx.commonHandler.appendParagraph(null);
410
+ return idx;
411
+ } : null;
412
+ };
413
+ var mappingForCode = {
414
+ "beforeStart": (ctx) => {
415
+ ctx.pctx.$codePx.autoComplete = false;
416
+ return null;
417
+ },
418
+ "afterEnd": (ctx) => {
419
+ ctx.pctx.$codePx.autoComplete = true;
420
+ return null;
421
+ },
422
+ " ": (_ctx, i, s) => {
423
+ if (s[i - 1] === " ") {
424
+ return {
425
+ type: "data",
426
+ value: " ",
427
+ nextIndex: i + 1
428
+ };
429
+ }
430
+ return null;
431
+ },
432
+ "`": (ctx, i, s) => {
433
+ if (s[i - 1] === "\n" && s[i + 1] === "`" && s[i + 2] === "`") {
434
+ return checkInCodeEnd(ctx, s[i + 3] === "\n" ? i + 4 : i + 3);
435
+ }
436
+ return null;
437
+ },
438
+ "\n": (ctx, i, s) => {
439
+ if (s[i + 1] === "`" && s[i + 2] === "`" && s[i + 3] === "`") {
440
+ return checkInCodeEnd(ctx, s[i + 4] === "\n" ? i + 5 : i + 4);
441
+ }
442
+ if (ctx.focusEtElement?.localName === "et-code") {
443
+ return {
444
+ type: "data",
445
+ value: "\n",
446
+ nextIndex: i + 1
447
+ };
448
+ }
449
+ return null;
450
+ }
451
+ };
452
+
453
+ // src/mapping/mappingForList.ts
454
+ var checkInUnorderedListHead = (ctx, i, s) => {
455
+ if (s[i + 1] !== " " || ctx.focusParagraph?.localName !== "et-li" || ctx.focusParagraph.textContent.length > 1) {
456
+ return null;
457
+ }
458
+ return () => i + 2;
459
+ };
460
+ var checkInOrderedListHead = (ctx, i, s) => {
461
+ if (ctx.focusParagraph?.localName !== "et-li" || ctx.focusParagraph.textContent.length > 1) {
462
+ return null;
463
+ }
464
+ return checkIsValidOrdered(i, s);
465
+ };
466
+ var checkIsValidOrdered = (i, s) => {
467
+ const j = s.indexOf(". ", i);
468
+ if (j === -1) {
469
+ return null;
470
+ }
471
+ const val = parseInt(s.slice(i, j));
472
+ if (isNaN(val)) {
473
+ return null;
474
+ }
475
+ return () => j + 2;
476
+ };
477
+ var indentToTab = (ctx, indent) => {
478
+ indent = Math.floor(indent / 4);
479
+ for (let j = 0; j < indent; j++) {
480
+ ctx.assists.ai.typeKey({ key: "Tab", code: "Tab" });
481
+ }
482
+ };
483
+ var mappingForList = {
484
+ "-": checkInUnorderedListHead,
485
+ "*": checkInUnorderedListHead,
486
+ "1": checkInOrderedListHead,
487
+ "2": checkInOrderedListHead,
488
+ "3": checkInOrderedListHead,
489
+ "4": checkInOrderedListHead,
490
+ "5": checkInOrderedListHead,
491
+ "6": checkInOrderedListHead,
492
+ "7": checkInOrderedListHead,
493
+ "8": checkInOrderedListHead,
494
+ "9": checkInOrderedListHead,
495
+ "\n": (ctx, i, s) => {
496
+ const n = s[i + 1];
497
+ if (n === "\n") {
498
+ if (ctx.focusEtElement?.localName === "et-li") {
499
+ return (_ctx) => {
500
+ _ctx.commonHandler.appendParagraph(null, { newP: ctx.createPlainParagraph() });
501
+ return i + 2;
502
+ };
503
+ }
504
+ } else if ((n === "-" || n === "*") && s[i + 2] === " ") {
505
+ if (ctx.focusEtElement?.localName === "et-li") {
506
+ return {
507
+ value: "Enter",
508
+ type: "key",
509
+ nextIndex: i + 3
510
+ };
511
+ }
512
+ } else {
513
+ for (let j = 1; j < 5; j++) {
514
+ if (s[i + j] === ".") {
515
+ if (s[i + j + 1] === " " && Number.isInteger(Number(s.slice(i, i + j)))) {
516
+ return {
517
+ value: "Enter",
518
+ type: "key",
519
+ nextIndex: i + j + 2
520
+ };
521
+ }
522
+ break;
523
+ }
524
+ }
525
+ }
526
+ return null;
527
+ },
528
+ " ": (ctx, i, s) => {
529
+ if (i !== 0 || s[i - 1] !== "\n" || ctx.focusParagraph?.localName !== "et-li") {
530
+ return null;
531
+ }
532
+ let indent = 1;
533
+ while (s[i + 1] === " ") {
534
+ i++;
535
+ indent++;
536
+ }
537
+ const nCode = s[i + 1]?.charCodeAt(0);
538
+ if (!nCode) {
539
+ return null;
540
+ }
541
+ if (nCode === 42 || nCode === 45) {
542
+ if (s[i + 2] !== " ") {
543
+ return null;
544
+ }
545
+ return () => {
546
+ indentToTab(ctx, indent);
547
+ return i + 3;
548
+ };
549
+ }
550
+ if (nCode >= 48 && nCode <= 57) {
551
+ const check = checkIsValidOrdered(i, s);
552
+ if (!check) {
553
+ return null;
554
+ }
555
+ return () => {
556
+ indentToTab(ctx, indent);
557
+ return check();
558
+ };
559
+ }
560
+ return null;
561
+ }
562
+ };
563
+
564
+ // src/mapping/mappingForMark.ts
565
+ var startBackquoteNum = 0;
566
+ var checkInMark = (ctx, type) => {
567
+ const el = ctx.focusEtElement;
568
+ return el?.localName === "et-mark" && el.markType === type;
569
+ };
570
+ var checkToTabout = (ctx, i, s, type, char, dbl) => {
571
+ if (!checkInMark(ctx, type)) {
572
+ return null;
573
+ }
574
+ if (char === "`") {
575
+ if (s[i - 1] !== char && s[i + 1] !== char) {
576
+ return {
577
+ type: "key",
578
+ value: "Tab",
579
+ nextIndex: i + 1
580
+ };
581
+ }
582
+ return {
583
+ type: "data",
584
+ value: "`",
585
+ nextIndex: i + 1
586
+ };
587
+ } else if (dbl ? s[i + 1] === char : s[i - 1] !== char) {
588
+ return {
589
+ value: "Tab",
590
+ type: "key",
591
+ nextIndex: dbl ? i + 2 : i + 1
592
+ };
593
+ }
594
+ return null;
595
+ };
596
+ var mappingForMark = {
597
+ "*": (ctx, i, s) => {
598
+ if (s[i - 1] === "\n" && s[i + 1] === " ") {
599
+ return {
600
+ value: "*",
601
+ type: "data",
602
+ nextIndex: i + 1
603
+ };
604
+ }
605
+ return checkToTabout(ctx, i, s, "bold", "*", true) ?? checkToTabout(ctx, i, s, "italic", "*", false);
606
+ },
607
+ "`": (ctx, i, s) => {
608
+ const ret = checkToTabout(ctx, i, s, "code", "`", false);
609
+ if (ret) {
610
+ return ret;
611
+ }
612
+ if (s[i + 1] === " ") {
613
+ return {
614
+ type: "key",
615
+ value: "`",
616
+ nextIndex: i + 2
617
+ };
618
+ }
619
+ if (s[i + 1] !== "`") {
620
+ return null;
621
+ }
622
+ let j = i + 1;
623
+ while (s[j] === "`") {
624
+ j++;
625
+ }
626
+ if (s[j] === " ") {
627
+ const end = s.indexOf(" " + "`".repeat(j - i), j + 1);
628
+ if (end === -1) {
629
+ return {
630
+ type: "data",
631
+ value: s.slice(i, j + 1),
632
+ nextIndex: j + 1
633
+ };
634
+ }
635
+ startBackquoteNum = j - i;
636
+ return {
637
+ type: "key",
638
+ value: "`",
639
+ nextIndex: j + 1
640
+ };
641
+ }
642
+ return null;
643
+ },
644
+ " ": (ctx, i, s) => {
645
+ if (s[i + 1] !== "`") {
646
+ return null;
647
+ }
648
+ if (checkInMark(ctx, "code")) {
649
+ let nextIndex = i + 1;
650
+ if (s[i + 2] !== "`") {
651
+ nextIndex = i + 2;
652
+ } else if (s.slice(nextIndex, nextIndex + startBackquoteNum) === "`".repeat(startBackquoteNum)) {
653
+ nextIndex = nextIndex + startBackquoteNum;
654
+ }
655
+ if (nextIndex !== i + 1) {
656
+ return {
657
+ type: "key",
658
+ value: "Tab",
659
+ nextIndex
660
+ };
661
+ }
662
+ }
663
+ return null;
664
+ },
665
+ "~": (ctx, i, s) => {
666
+ return checkToTabout(ctx, i, s, "delete", "~", true);
667
+ },
668
+ "=": (ctx, i, s) => {
669
+ return checkToTabout(ctx, i, s, "highlight", "=", true);
670
+ }
671
+ };
672
+
673
+ // src/index.ts
674
+ var useAIAssist = (options) => {
675
+ options = {
676
+ markdownTextMappings: [
677
+ mappingForCode,
678
+ mappingForMark,
679
+ mappingForList,
680
+ mappingForBlockquote
681
+ ]
682
+ };
683
+ return {
684
+ name: "@effitor/assist-ai",
685
+ effector: [{
686
+ onMounted: (ctx) => {
687
+ ctx.assists.ai = new EffitorAI(ctx, options);
688
+ }
689
+ }]
690
+ };
691
+ };
692
+ export {
693
+ mappingForBlockquote,
694
+ mappingForCode,
695
+ mappingForList,
696
+ mappingForMark,
697
+ useAIAssist
698
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@effitor/assist-ai",
3
+ "version": "0.2.0",
4
+ "description": "A lightweight AI assistant for Effitor.",
5
+ "keywords": [
6
+ "editor",
7
+ "wysiwyg",
8
+ "markdown"
9
+ ],
10
+ "type": "module",
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ }
22
+ },
23
+ "author": "Henvn Leaf",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/Ausprain/effitor.git",
28
+ "directory": "packages/assist-ai"
29
+ },
30
+ "scripts": {
31
+ "build": "tsup"
32
+ },
33
+ "dependencies": {
34
+ "@effitor/core": "workspace:*"
35
+ }
36
+ }