@pfmcodes/caret 0.2.9 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,503 @@
1
+ /**
2
+ * createTextEditor {
3
+ * @param {HTMLElement} parent - The element to append the editor to
4
+ * @param {string} content - Initial content of the editor, default is ""
5
+ * @param {string|number} id - Unique ID for this instance, used for undo/redo stack namespacing
6
+ * @param {Object} options - Optional configuration {
7
+ * @param {boolean} dark - dark theme enabled, false by default
8
+ * @param {boolean} shadow - box shadow enabled, true by default
9
+ * @param {string} focusColor - border color on focus, default #7c3aed
10
+ * @param {string} shadowColor - shadow color, default #000
11
+ * @param {boolean} lock - read-only mode, false by default
12
+ * @param {string} language - highlight.js language, default "plaintext"
13
+ * @param {string} hlTheme - highlight.js theme name, default "hybrid"
14
+ * @param {Object} font {
15
+ * @param {string} url - font file URL, only needed for external/custom fonts
16
+ * @param {string} name - font name, required for custom fonts
17
+ * }
18
+ * @param {Object} theme - custom colors {
19
+ * @param {Object} dark {
20
+ * @param {string} background.editor - editor background color
21
+ * @param {string} background.lineCounter - line counter background color
22
+ * @param {string} color.editor - editor text color
23
+ * @param {string} color.lineCounter - line counter text color
24
+ * @param {string} editor.caret - caret color
25
+ * }
26
+ * @param {Object} light {
27
+ * @param {string} background.editor - editor background color
28
+ * @param {string} background.lineCounter - line counter background color
29
+ * @param {string} color.editor - editor text color
30
+ * @param {string} color.lineCounter - line counter text color
31
+ * @param {string} editor.caret - caret color
32
+ * }
33
+ * }
34
+ * }
35
+ * @returns {Promise<Object>} {
36
+ * @return {function} getValue - returns current editor content (strips \u200B)
37
+ * @return {function} setValue - (@param {string} val) sets editor content
38
+ * @return {function} getCursor - returns current cursor offset
39
+ * @return {function} setCursor - (@param {number} pos) moves cursor to position
40
+ * @return {function} undo - undoes last change, restores cursor position
41
+ * @return {function} redo - redoes last undone change, restores cursor position
42
+ * @return {function} onChange - (@param {function} fn) fires on every content change with new text
43
+ * @return {function} onCursorMove - (@param {function} fn) fires on every cursor move with new position
44
+ * @return {function} isFocused - returns true if editor is currently focused
45
+ * @return {function} setLanguage - (@param {string} lang) switches syntax highlighting language
46
+ * @return {string} id - the editor instance id
47
+ * @return {function} delete - destroys editor, removes DOM elements and event listeners
48
+ * }
49
+ *
50
+ * @notes
51
+ * - Requires Chrome/Chromium — uses EditContext API (not supported in Firefox/Safari yet)
52
+ * - Undo/redo stacks accessible globally via window.caret[`undoStack.${id}`]
53
+ * - \u200B (zero-width space) used internally for newline rendering, stripped from getValue()
54
+ * - Keyboard shortcuts: Ctrl+Z undo, Ctrl+Y / Ctrl+Shift+Z redo, Tab indent, Shift+Tab unindent
55
+ * }
56
+ */
57
+
58
+ import { createLineCounter, updateLineCounter } from "./lineCounter.js";
59
+ import { loadFont } from "./font.js";
60
+ import { createCaret } from "./caret.js";
61
+ // @ts-ignore
62
+ import hljs from "https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/core.js";
63
+ import languages from "./languages.js";
64
+
65
+ languages.init();
66
+
67
+ async function createTextEditor(parent, content = "", id, options = {}) {
68
+ let onCursorMoveFn = null;
69
+ async function isChromiumEngine() {
70
+ if (navigator.userAgentData) {
71
+ return navigator.userAgentData.brands.some(b => b.brand === 'Chromium');
72
+ }
73
+ const ua = navigator.userAgent;
74
+ return /Chrome/i.test(ua) && !/Edg/i.test(ua) && !/OPR/i.test(ua);
75
+ }
76
+
77
+ async function getBrowserName() {
78
+ if (navigator.userAgentData) {
79
+ const brands = navigator.userAgentData.brands;
80
+ const primaryBrand = brands.find(b => b.brand !== 'Chromium' && b.brand !== 'Not(A:Brand)') || brands[0];
81
+ return primaryBrand.brand;
82
+ }
83
+
84
+ const ua = navigator.userAgent;
85
+ if (ua.includes("Firefox")) return "Mozilla Firefox";
86
+ if (ua.includes("SamsungBrowser")) return "Samsung Internet";
87
+ if (ua.includes("Opera") || ua.includes("OPR")) return "Opera";
88
+ if (ua.includes("Trident")) return "Internet Explorer";
89
+ if (ua.includes("Edge")) return "Microsoft Edge (Legacy)";
90
+ if (ua.includes("Edg")) return "Microsoft Edge";
91
+ if (ua.includes("Chrome")) return "Google Chrome";
92
+ if (ua.includes("Safari")) return "Apple Safari";
93
+
94
+ return "Unknown Browser";
95
+ }
96
+
97
+ isChromiumEngine().then(async isChromium => {
98
+ if (!isChromium) {
99
+ const main = document.createElement("div");
100
+ main.style = "display: flex; align-items: center; justify-content: center; padding: 20px; white-space: pre-wrap; margin: 0 auto;";
101
+ main.innerHTML = `<h2>Caret (editor engine) does not yet support ${await getBrowserName()}.<br>File an issue <a href="https://github.com/PFMCODES/Caret/issues">here.</a></h2>`;
102
+ parent.appendChild(main);
103
+ }
104
+ });
105
+ if (id === undefined || id === null || (typeof id !== "string" && typeof id !== "number")) {
106
+ console.error(`parameter 'id' of function createTextEditor must not be '${typeof id}', it must be a number or string`);
107
+ return;
108
+ }
109
+ if (!parent || !(parent instanceof HTMLElement)) {
110
+ console.error(`'parent' parameter of function 'createTextEditor' must be an HTMLElement`);
111
+ return;
112
+ }
113
+ if (!("EditContext" in window)) {
114
+ console.error("EditContext API is not supported in ", await getBrowserName());
115
+ return;
116
+ }
117
+
118
+ if (!window.caret) window.caret = {};
119
+ window.caret[`undoStack.${id}`] = [{ content, cursor: 0 }];
120
+ window.caret[`redoStack.${id}`] = [];
121
+
122
+ const lock = options.lock || false;
123
+ const focusColor = options.focusColor || '#7c3aed';
124
+ const dark = options.dark || false;
125
+ const boxShadow = options.shadow ?? true;
126
+ const shadowColor = options.shadowColor || "#000";
127
+ const theme = options.theme;
128
+ const font = options.font || {};
129
+ let language = options.language || "plaintext";
130
+
131
+ const themeLink = document.createElement("link");
132
+ themeLink.rel = "stylesheet";
133
+ themeLink.id = `caret-theme-${id}`;
134
+ themeLink.href = options.hlTheme
135
+ ? `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/${options.hlTheme}.css`
136
+ : `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/hybrid.css`;
137
+ document.head.appendChild(themeLink);
138
+
139
+ if (!languages.registeredLanguages.includes(language)) {
140
+ const mod = await import(`https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/languages/${language}.js`);
141
+ languages.registerLanguage(language, mod.default);
142
+ }
143
+
144
+ let fontName;
145
+ if (!font.url && !font.name) {
146
+ fontName = "monospace";
147
+ parent.style.fontFamily = fontName;
148
+ } else if (!font.url && font.name) {
149
+ parent.style.fontFamily = font.name;
150
+ } else {
151
+ fontName = font.name;
152
+ loadFont(fontName, font.url);
153
+ parent.style.fontFamily = fontName;
154
+ }
155
+
156
+ let text = content;
157
+ let selStart = content.length;
158
+ let selEnd = content.length;
159
+ let isFocused = false;
160
+ let onChangeFn = null;
161
+
162
+ const editContext = new EditContext({
163
+ text,
164
+ selectionStart: selStart,
165
+ selectionEnd: selEnd
166
+ });
167
+
168
+ const main = document.createElement("div");
169
+ main.editContext = editContext;
170
+ main.tabIndex = 0;
171
+ main.style.whiteSpace = "pre";
172
+ main.style.height = "100%";
173
+ main.style.width = "100%";
174
+ main.style.minWidth = "50px";
175
+ main.style.minHeight = "25px";
176
+ main.style.fontSize = "14px";
177
+ main.style.lineHeight = "1.5";
178
+ main.style.outline = "none";
179
+ main.style.boxSizing = "border-box";
180
+ main.style.borderTopRightRadius = "5px";
181
+ main.style.borderBottomRightRadius = "5px";
182
+ main.style.transition = "all 0.2s ease-in-out";
183
+ main.style.display = "block";
184
+ main.style.paddingTop = "5px";
185
+ main.style.overflowX = "auto";
186
+ main.style.scrollbarWidth = "none";
187
+ main.style.cursor = "text";
188
+ main.style.userSelect = "none";
189
+ main.style.caretColor = "transparent";
190
+
191
+ if (boxShadow) main.style.boxShadow = `1px 1px 1px 1px ${shadowColor}`;
192
+
193
+ if (!theme) {
194
+ main.style.background = dark ? "#101010" : "#d4d4d4";
195
+ main.style.color = dark ? "#fff" : "#000";
196
+ } else {
197
+ main.style.background = dark ? theme.dark["background.editor"] : theme.light["background.editor"];
198
+ main.style.color = dark ? theme.dark["color.editor"] : theme.light["color.editor"];
199
+ }
200
+
201
+ let caretColor;
202
+ if (options.theme) {
203
+ caretColor = dark ? options.theme.dark["editor.caret"] : options.theme.light["editor.caret"];
204
+ } else {
205
+ caretColor = "#fff";
206
+ }
207
+
208
+ parent.style.display = "flex";
209
+ parent.style.alignItems = "flex-start";
210
+ parent.style.border = "2px solid #0000";
211
+ parent.style.padding = "5px";
212
+ parent.style.position = "relative";
213
+
214
+ let lineCounter;
215
+ lineCounter = await createLineCounter(parent, content.split("\n").length, id, options);
216
+
217
+ parent.appendChild(main);
218
+
219
+ const caret = createCaret(parent, main, { ...options, caretColor });
220
+
221
+ function render() {
222
+ let displayText = text;
223
+ let od = 0;
224
+ if (displayText.endsWith("\n")) {
225
+ displayText += "\u200B";
226
+ od = 1;
227
+ }
228
+ const highlighted = hljs.highlight(displayText, { language }).value;
229
+ main.innerHTML = highlighted;
230
+
231
+ updateLineCounter(lineCounter, text.trimEnd().split("\n").length + od);
232
+ caret.update(selStart);
233
+ if (onChangeFn) onChangeFn(text);
234
+ }
235
+
236
+ function saveState() {
237
+ const stack = window.caret[`undoStack.${id}`];
238
+ if (text !== stack[stack.length - 1]?.content) {
239
+ stack.push({ content: text, cursor: selStart });
240
+ window.caret[`redoStack.${id}`] = [];
241
+ }
242
+ }
243
+
244
+ function undo() {
245
+ const stack = window.caret[`undoStack.${id}`];
246
+ const redoStack = window.caret[`redoStack.${id}`];
247
+ if (stack.length <= 1) return;
248
+ const current = stack.pop();
249
+ redoStack.push(current);
250
+ const prev = stack[stack.length - 1];
251
+ text = prev.content;
252
+ const diff = current.content.length - prev.content.length;
253
+ selStart = selEnd = Math.max(0, current.cursor - diff);
254
+ editContext.updateText(0, editContext.text.length, text);
255
+ editContext.updateSelection(selStart, selEnd);
256
+ render();
257
+ }
258
+
259
+ function redo() {
260
+ const stack = window.caret[`undoStack.${id}`];
261
+ const redoStack = window.caret[`redoStack.${id}`];
262
+ if (redoStack.length === 0) return;
263
+ const next = redoStack.pop();
264
+ stack.push(next);
265
+ text = next.content;
266
+ selStart = selEnd = next.cursor;
267
+ editContext.updateText(0, editContext.text.length, text);
268
+ editContext.updateSelection(selStart, selEnd);
269
+ render();
270
+ }
271
+
272
+ editContext.addEventListener("textupdate", (e) => {
273
+ if (lock) return;
274
+ text = text.slice(0, e.updateRangeStart) + e.text + text.slice(e.updateRangeEnd);
275
+ text = text.replaceAll("\u200B", "");
276
+ selStart = selEnd = e.selectionStart;
277
+ editContext.updateText(0, editContext.text.length, text);
278
+ saveState();
279
+ render();
280
+ });
281
+
282
+ editContext.addEventListener("selectionchange", (e) => {
283
+ selStart = e.selectionStart;
284
+ selEnd = e.selectionEnd;
285
+ caret.update(selStart);
286
+ });
287
+
288
+ main.addEventListener("keydown", (e) => {
289
+ if (lock) return;
290
+
291
+ if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === "z") {
292
+ e.preventDefault();
293
+ undo();
294
+ return;
295
+ }
296
+ if ((e.ctrlKey || e.metaKey) && e.key === "y") {
297
+ e.preventDefault();
298
+ redo();
299
+ return;
300
+ }
301
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "Z") {
302
+ e.preventDefault();
303
+ redo();
304
+ return;
305
+ }
306
+
307
+ if (e.key === "Tab" && !e.shiftKey) {
308
+ e.preventDefault();
309
+ const indent = " ";
310
+ if (selStart !== selEnd) {
311
+ const before = text.slice(0, selStart);
312
+ const selected = text.slice(selStart, selEnd);
313
+ const after = text.slice(selEnd);
314
+ const indented = selected.split("\n").map(l => indent + l).join("\n");
315
+ text = before + indented + after;
316
+ selEnd = selStart + indented.length;
317
+ } else {
318
+ text = text.slice(0, selStart) + indent + text.slice(selStart);
319
+ selStart = selEnd = selStart + indent.length;
320
+ }
321
+ editContext.updateText(0, editContext.text.length, text);
322
+ editContext.updateSelection(selStart, selEnd);
323
+ saveState();
324
+ render();
325
+ return;
326
+ }
327
+
328
+ if (e.key === "Enter") {
329
+ e.preventDefault();
330
+ const newText = text.slice(0, selStart) + "\n" + "\u200B" + text.slice(selEnd);
331
+ text = newText;
332
+ selStart = selEnd = selStart + 1;
333
+ editContext.updateText(0, text.length, text);
334
+ editContext.updateSelection(selStart, selEnd);
335
+ saveState();
336
+ render();
337
+ return;
338
+ }
339
+
340
+ if (e.shiftKey && e.key === "Tab") {
341
+ e.preventDefault();
342
+ if (selStart !== selEnd) {
343
+ const before = text.slice(0, selStart);
344
+ const selected = text.slice(selStart, selEnd);
345
+ const after = text.slice(selEnd);
346
+ const unindented = selected.split("\n").map(l =>
347
+ l.startsWith(" ") ? l.slice(4) :
348
+ l.startsWith("\t") ? l.slice(1) : l
349
+ ).join("\n");
350
+ text = before + unindented + after;
351
+ selEnd = selStart + unindented.length;
352
+ } else {
353
+ const lineStart = text.lastIndexOf("\n", selStart - 1) + 1;
354
+ const linePrefix = text.slice(lineStart, lineStart + 4);
355
+ if (linePrefix === " ") {
356
+ text = text.slice(0, lineStart) + text.slice(lineStart + 4);
357
+ selStart = selEnd = Math.max(lineStart, selStart - 4);
358
+ }
359
+ }
360
+ editContext.updateText(0, editContext.text.length, text);
361
+ editContext.updateSelection(selStart, selEnd);
362
+ saveState();
363
+ render();
364
+ return;
365
+ }
366
+ if (e.key === "ArrowLeft") {
367
+ e.preventDefault();
368
+ selStart = selEnd = Math.max(0, selStart - 1);
369
+ editContext.updateSelection(selStart, selEnd);
370
+ caret.update(selStart);
371
+ if (onCursorMoveFn) onCursorMoveFn(selStart);
372
+ return;
373
+ }
374
+
375
+ if (e.key === "ArrowRight") {
376
+ e.preventDefault();
377
+ selStart = selEnd = Math.min(text.length, selStart + 1);
378
+ editContext.updateSelection(selStart, selEnd);
379
+ caret.update(selStart);
380
+ if (onCursorMoveFn) onCursorMoveFn(selStart);
381
+ return;
382
+ }
383
+
384
+ if (e.key === "ArrowUp") {
385
+ e.preventDefault();
386
+ const lineStart = text.lastIndexOf("\n", selStart - 1) + 1;
387
+ const prevLineEnd = lineStart - 1;
388
+ const prevLineStart = text.lastIndexOf("\n", prevLineEnd - 1) + 1;
389
+ const col = selStart - lineStart;
390
+ const prevLineLength = prevLineEnd - prevLineStart;
391
+ selStart = selEnd = prevLineStart + Math.min(col, prevLineLength);
392
+ editContext.updateSelection(selStart, selEnd);
393
+ caret.update(selStart);
394
+ if (onCursorMoveFn) onCursorMoveFn(selStart);
395
+ return;
396
+ }
397
+
398
+ if (e.key === "ArrowDown") {
399
+ e.preventDefault();
400
+ const lineStart = text.lastIndexOf("\n", selStart - 1) + 1;
401
+ const nextLineStart = text.indexOf("\n", selStart) + 1;
402
+ const nextLineEnd = text.indexOf("\n", nextLineStart);
403
+ const finalNextLineEnd = nextLineEnd === -1 ? text.length : nextLineEnd;
404
+ const col = selStart - lineStart;
405
+ const nextLineLength = finalNextLineEnd - nextLineStart;
406
+ selStart = selEnd = nextLineStart + Math.min(col, nextLineLength);
407
+ editContext.updateSelection(selStart, selEnd);
408
+ caret.update(selStart);
409
+ if (onCursorMoveFn) onCursorMoveFn(selStart);
410
+ return;
411
+ }
412
+ });
413
+
414
+ main.addEventListener("paste", (e) => {
415
+ if (lock) return;
416
+ e.preventDefault();
417
+ const pasteText = e.clipboardData.getData("text/plain");
418
+ text = text.slice(0, selStart) + pasteText + text.slice(selEnd);
419
+ selStart = selEnd = selStart + pasteText.length;
420
+ editContext.updateText(0, editContext.text.length, text);
421
+ editContext.updateSelection(selStart, selEnd);
422
+ saveState();
423
+ render();
424
+ });
425
+
426
+ main.addEventListener("focus", () => {
427
+ isFocused = true;
428
+ parent.style.border = `2px solid ${focusColor}`;
429
+ parent.style.boxShadow = "none";
430
+ caret.show();
431
+ caret.update(selStart);
432
+ });
433
+
434
+ main.addEventListener("blur", () => {
435
+ isFocused = false;
436
+ parent.style.border = "2px solid #0000";
437
+ if (boxShadow) parent.style.boxShadow = `1px 1px 1px 1px ${shadowColor}`;
438
+ caret.hide();
439
+ });
440
+
441
+ main.addEventListener("click", (e) => {
442
+ main.focus();
443
+ const range = document.caretRangeFromPoint(e.clientX, e.clientY);
444
+ if (!range) return;
445
+
446
+ let offset = 0;
447
+ let remaining = 0;
448
+ const walker = document.createTreeWalker(main, NodeFilter.SHOW_TEXT);
449
+ let node;
450
+ while ((node = walker.nextNode())) {
451
+ if (node === range.startContainer) {
452
+ offset = remaining + range.startOffset;
453
+ break;
454
+ }
455
+ remaining += node.textContent.length;
456
+ }
457
+
458
+ selStart = selEnd = offset;
459
+ editContext.updateSelection(selStart, selEnd);
460
+ caret.update(selStart);
461
+ });
462
+
463
+ render();
464
+
465
+ return {
466
+ getValue: () => text.replaceAll("\u200B", ""),
467
+ getCursor: () => selStart,
468
+ setCursor: (pos) => {
469
+ selStart = selEnd = Math.max(0, Math.min(pos, text.length));
470
+ editContext.updateSelection(selStart, selEnd);
471
+ caret.update(selStart);
472
+ },
473
+ undo: undo,
474
+ redo: redo,
475
+ setValue: (val) => {
476
+ text = val.replaceAll("\u200B", "");
477
+ selStart = selEnd = text.length;
478
+ editContext.updateText(0, editContext.text.length, text);
479
+ editContext.updateSelection(selStart, selEnd);
480
+ render();
481
+ },
482
+ id: options.id,
483
+ onChange: (fn) => { onChangeFn = fn; },
484
+ isFocused: () => isFocused,
485
+ setLanguage: async (lang) => {
486
+ if (!languages.registeredLanguages.includes(lang)) {
487
+ const mod = await import(`https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/languages/${lang}.js`);
488
+ languages.registerLanguage(lang, mod.default);
489
+ }
490
+ language = lang;
491
+ render();
492
+ },
493
+ delete: () => {
494
+ parent.removeChild(main);
495
+ parent.removeChild(lineCounter);
496
+ caret.destroy();
497
+ document.head.removeChild(themeLink);
498
+ parent.style = "";
499
+ }
500
+ };
501
+ }
502
+
503
+ export { createTextEditor }
package/index.js CHANGED
@@ -1,22 +1,7 @@
1
- import editor from "./editor.js";
2
- import theme from "./theme.js";
3
- import language from "./langauges.js";
4
-
1
+ import * as t from "./components/textEditor.js";
5
2
  const Caret = {
6
- editor,
7
- theme,
8
- language
3
+ createEditor: t.createTextEditor,
4
+ createTextEditor: t.createTextEditor
9
5
  }
10
- export default Caret;
11
6
 
12
- /*
13
- Caret.editor:
14
- createEditor() -> backbone of caret, handles ui and abstractions
15
- Caret.theme:
16
- setTheme() -> changes the current highlight.js theme
17
- Caret.langauge:
18
- init() -> initializes default avaible languages
19
- registerLanguage() -> registers a new languages
20
- registeredLangauges[List]: has all the langauges registered
21
- hljs: the highlight.js module
22
- */
7
+ export default Caret;
package/package.json CHANGED
@@ -1,19 +1,9 @@
1
1
  {
2
2
  "name": "@pfmcodes/caret",
3
- "version": "0.2.9",
3
+ "version": "0.3.1",
4
4
  "description": "The official code editor engine for lexius",
5
5
  "type": "module",
6
6
  "main": "./index.js",
7
- "types": "./types/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./types/index.d.ts",
11
- "default": "./index.js"
12
- },
13
- "./editor": "./esm/index.js",
14
- "./langauges": "./langauges.js",
15
- "./theme": "./theme.js"
16
- },
17
7
  "repository": {
18
8
  "type": "git",
19
9
  "url": "git+https://github.com/PFMCODES/lexius-editor.git"
@@ -37,5 +27,5 @@
37
27
  "bugs": {
38
28
  "url": "https://github.com/PFMCODES/Caret/issues"
39
29
  },
40
- "homepage": "https://github.com/PFMCODES/Caret#readme"
41
- }
30
+ "homepage": "https://pfmcodes.onrender.com/apps/caret/"
31
+ }
package/utilities.js ADDED
@@ -0,0 +1,32 @@
1
+ export function isMoreThanOneChange(s1, s2) {
2
+ const len1 = s1.length;
3
+ const len2 = s2.length;
4
+
5
+ // 1. If length difference is > 1, they definitely have > 1 change
6
+ if (Math.abs(len1 - len2) > 1) return true;
7
+
8
+ let count = 0;
9
+ let i = 0, j = 0;
10
+
11
+ while (i < len1 && j < len2) {
12
+ if (s1[i] !== s2[j]) {
13
+ count++;
14
+ if (count > 1) return true; // Exit early
15
+
16
+ if (len1 > len2) {
17
+ i++; // Character deleted in s2
18
+ } else if (len1 < len2) {
19
+ j++; // Character added in s2
20
+ } else {
21
+ i++; j++; // Character replaced
22
+ }
23
+ } else {
24
+ i++; j++;
25
+ }
26
+ }
27
+
28
+ // Check for a trailing character difference at the end
29
+ if (i < len1 || j < len2) count++;
30
+
31
+ return count > 1;
32
+ }