@pfmcodes/caret 0.2.1 → 0.2.6

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/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@pfmcodes/caret",
3
- "version": "0.2.1",
3
+ "version": "0.2.6",
4
4
  "description": "The official code editor engine for lexius",
5
5
  "type": "module",
6
- "main": "./esm/index.js",
6
+ "main": "./index.js",
7
7
  "types": "./types/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
10
  "types": "./types/index.d.ts",
11
- "import": "./esm/index.js",
12
- "require": "./commonjs/index.js"
11
+ "default": "./index.js"
13
12
  },
14
- "./commonjs": "./commonjs/index.js",
15
- "./esm": "./esm/index.js"
13
+ "./editor": "./esm/index.js",
14
+ "./langauges": "./langauges.js",
15
+ "./theme": "./theme.js"
16
16
  },
17
17
  "repository": {
18
18
  "type": "git",
@@ -35,7 +35,7 @@
35
35
  "author": "PFMCODES",
36
36
  "license": "MIT",
37
37
  "bugs": {
38
- "url": "https://github.com/PFMCODES/lexius-editor/issues"
38
+ "url": "https://github.com/PFMCODES/Caret/issues"
39
39
  },
40
- "homepage": "https://github.com/PFMCODES/lexius-editor#readme"
40
+ "homepage": "https://github.com/PFMCODES/Caret#readme"
41
41
  }
package/types/editor.ts CHANGED
@@ -3,39 +3,45 @@ import languages from "./languages.ts";
3
3
 
4
4
  languages.init();
5
5
 
6
- async function createEditor(editor: HTMLElement, data: { value?: string, language: string, theme?: string }) {
7
- const editor1: HTMLTextAreaElement = document.createElement("textarea");
8
- const highlighted: HTMLPreElement = document.createElement("pre");
9
- const caret: HTMLDivElement = document.createElement("div");
10
- const measureCanvas: HTMLCanvasElement = document.createElement("canvas");
11
- const measureCtx: CanvasRenderingContext2D = measureCanvas.getContext("2d") as CanvasRenderingContext2D;
12
- if (!measureCtx) {
13
- throw new Error("Failed to get 2D context from canvas");
14
- }
6
+ async function createEditor(editor: HTMLElement, data: any) {
7
+ const editor1 = document.createElement("textarea");
8
+ const highlighted = document.createElement("pre");
9
+ const caret = document.createElement("div");
10
+ const measureCanvas = document.createElement("canvas");
11
+ const measureCtx: any = measureCanvas.getContext("2d");
15
12
  const isDark = data.theme && (data.theme.includes("dark") || data.theme.includes("night"));
16
13
  const caretColor = isDark ? "#fff" : "#7116d8";
17
14
  const lineColor = isDark ? "#fff" : "#000";
18
- const lineCounter: HTMLDivElement = document.createElement("div");
19
-
20
- editor1.id = "Caret-textarea";
21
- highlighted.id = "Caret-highlighted";
22
- caret.id = "Caret-caret";
23
- lineCounter.id = "Caret-lineCounter";
24
- editor1.className = 'dark';
25
- highlighted.className = 'dark';
26
- caret.className = 'dark';
27
- lineCounter.className = 'dark';
15
+ const lineCounter = document.createElement("div");
16
+
17
+ const i = Math.random().toString(16).slice(2);
18
+ editor1.id = `Caret-textarea-${i}`;
19
+ highlighted.id = `Caret-highlighted-${i}`;
20
+ caret.id = `Caret-caret-${i}`;
21
+ lineCounter.id = `Caret-lineCounter-${i}`;
22
+ editor1.classList.add("Caret-textarea");
23
+ highlighted.classList.add("Caret-highlighted");
24
+ caret.classList.add("Caret-caret");
25
+ lineCounter.classList.add("Caret-lineCounter");
26
+ editor1.classList.add("dark");
27
+ highlighted.classList.add("dark");
28
+ caret.classList.add("dark");
29
+ lineCounter.classList.add("dark");
28
30
  editor1.style.backgroundColor = isDark ? "#222" : "#fff";
29
31
  let code = data.value || "";
30
32
  let language = data.language;
31
33
  let theme = data.theme;
34
+ let lock = data.readOnly || false;
35
+ if (lock) {
36
+ editor1.readOnly = true;
37
+ }
32
38
  if (!languages.registeredLanguages.includes(language)) {
33
39
  const mod = await import(`https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/languages/${language}.js`);
34
40
  languages.registerLanguage(language, mod.default);
35
41
  languages.registeredLanguages.push(language);
36
42
  }
37
43
  if (theme) {
38
- let themeLink: HTMLLinkElement | null = document.getElementById("Caret-theme") as HTMLLinkElement;
44
+ let themeLink = document.getElementById("Caret-theme")
39
45
  if (!themeLink) {
40
46
  const link = document.createElement("link");
41
47
  link.rel = "stylesheet";
@@ -46,7 +52,7 @@ async function createEditor(editor: HTMLElement, data: { value?: string, languag
46
52
  themeLink.href = `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/${theme}.css`;
47
53
  }
48
54
  } else {
49
- let themeLink: HTMLLinkElement | null = document.getElementById("Caret-theme") as HTMLLinkElement;
55
+ let themeLink = document.getElementById("Caret-theme");
50
56
  if (!themeLink) {
51
57
  const link = document.createElement("link");
52
58
  link.rel = "stylesheet";
@@ -58,17 +64,16 @@ async function createEditor(editor: HTMLElement, data: { value?: string, languag
58
64
  }
59
65
  }
60
66
  editor1.spellcheck = false;
61
- editor1.autocapitalize = "off";
62
- editor1.autocomplete = "off";
63
- editor1.autocorrect = "off" as any;
67
+ (editor1 as any).autocapitalize = "off";
68
+ (editor1 as any).autocomplete = "off";
69
+ (editor1 as any).autocorrect = false;
64
70
  editor.style = "position: relative; width: 600px; height: 300px; overflow: hidden; /* 👈 CRITICAL */ font-size: 14px;"
65
71
  if (code && editor && editor1 && language && highlighted) {
66
72
  editor1.style.paddingTop = "-9px";
67
- console.log(data.value + " data.value");
68
- editor1.value = data.value as string;
69
- highlighted.innerHTML = await _render(code, language, editor1);
73
+ editor1.value = data.value;
74
+ highlighted.innerHTML = await _render(data.value, language, editor1);
70
75
  }
71
- const keyDown = async (e: KeyboardEvent) => {
76
+ const keyDown = async (e: any) => {
72
77
  if (e.key !== "Tab") return;
73
78
 
74
79
  e.preventDefault();
@@ -137,12 +142,15 @@ async function createEditor(editor: HTMLElement, data: { value?: string, languag
137
142
  }
138
143
 
139
144
  function updateLineNumbers() {
140
- const lineCount = editor1.value.split("\n").length;
145
+ const lines = editor1.value.split("\n");
146
+ const wrapMap = getWrapMap(editor1.value, 71);
141
147
 
142
148
  let html = "";
143
- for (let i = 1; i <= lineCount; i++) {
144
- html += `<div class="Caret-lineCounter-number" style="color: ${lineColor}">${i}</div>`;
145
- }
149
+ lines.forEach((line, i) => {
150
+ const visualLines = wrapMap[i] || 1;
151
+ // first visual line gets the number
152
+ html += `<div class="Caret-lineCounter-number" style="color: ${lineColor}; height: calc(1.5em * ${visualLines})">${i + 1}</div>`;
153
+ });
146
154
 
147
155
  lineCounter.innerHTML = html;
148
156
  }
@@ -170,47 +178,92 @@ async function createEditor(editor: HTMLElement, data: { value?: string, languag
170
178
 
171
179
  editor1.addEventListener("blur", blur);
172
180
 
181
+ function getVisualRow(text: string, wrapAt: number) {
182
+ // simulate exactly what wrapCode does, but track caret position
183
+ const words = text.split(" ");
184
+ let currentLine = "";
185
+ let visualRow = 0;
186
+
187
+ for (let w = 0; w < words.length; w++) {
188
+ const word = words[w];
189
+ const isLast = w === words.length - 1;
190
+ const test = currentLine ? currentLine + " " + word : word;
191
+
192
+ if (test.length > wrapAt && currentLine !== "") {
193
+ visualRow++;
194
+ currentLine = word;
195
+ } else {
196
+ currentLine = test;
197
+ }
198
+ }
199
+
200
+ return { row: visualRow, lineText: currentLine };
201
+ }
202
+
173
203
  function updateCaret() {
174
204
  const start = editor1.selectionStart;
175
205
  const text = editor1.value.slice(0, start);
176
206
 
177
207
  const lines = text.split("\n");
178
208
  const lineIndex = lines.length - 1;
179
- const lineText = lines[lineIndex].replace(/\t/g, " ");
209
+ const currentLineText = lines[lineIndex].replace(/\t/g, " ");
210
+
180
211
 
181
212
  const style = getComputedStyle(editor1);
182
213
  const paddingLeft = parseFloat(style.paddingLeft);
183
214
  const paddingTop = parseFloat(style.paddingTop);
184
- const lineHeight = parseFloat(style.lineHeight);
215
+ const lineHeight = parseFloat(style.lineHeight) || 20;
185
216
 
186
217
  updateFontMetrics();
187
- const metrics = measureCtx.measureText("Mg");
188
- const ascent = metrics.actualBoundingBoxAscent;
218
+ const ascent = measureCtx.measureText("Mg").actualBoundingBoxAscent;
219
+
220
+ const WRAP_AT = 71;
221
+
222
+ // count visual rows from all previous lines
223
+ let totalVisualRows = 0;
224
+ for (let i = 0; i < lineIndex; i++) {
225
+ const l = lines[i].replace(/\t/g, " ");
226
+ const { row } = getVisualRow(l, WRAP_AT);
227
+ totalVisualRows += row + 1;
228
+ }
229
+
230
+ const { row, lineText } = getVisualRow(currentLineText, WRAP_AT);
231
+ const onWrappedLine = row > 0;
189
232
 
190
- caret.style.left =
191
- paddingLeft + measureCtx.measureText(lineText).width + "px";
192
- caret.style.top =
193
- -9 +
194
- paddingTop +
195
- lineIndex * lineHeight +
196
- (lineHeight - ascent) +
197
- "px";
233
+ totalVisualRows += row;
198
234
 
235
+ if (onWrappedLine) {
236
+ // on wrapped line - X is just the text on this visual row
237
+
238
+ caret.style.left = paddingLeft + measureCtx.measureText(lineText).width + "px";
239
+ } else {
240
+ // on original line - X is full line text width
241
+
242
+ caret.style.left = paddingLeft + measureCtx.measureText(lineText.trimEnd()).width + "px";
243
+ }
244
+
245
+ caret.style.top = -9 + paddingTop + totalVisualRows * lineHeight + (lineHeight - ascent) + "px";
199
246
  caret.style.height = `${lineHeight - 5}px`;
200
247
  }
248
+
201
249
  const input = async () => {
202
250
  caret.style.opacity = "1";
203
251
  highlighted.innerHTML = await _render(editor1.value, language, editor1);
204
252
  updateLineNumbers();
205
253
  updateCaret();
206
254
  };
255
+
256
+ const onDidChangeModelContent = (fn: any) => {
257
+ editor1.addEventListener("input", fn)
258
+ }
259
+
207
260
  editor1.addEventListener("input", input);
208
261
  const scroll = async () => {
209
262
  const x = -editor1.scrollLeft;
210
263
  const y = -editor1.scrollTop;
211
264
  highlighted.innerHTML = await _render(editor1.value, language, editor1);
212
- highlighted.style.transform = `translate(${x}px, ${y}px)`;
213
- caret.style.transform = `translate(${x}px, ${y}px)`;
265
+ highlighted.style.transform = `translateY(${y}px)`;
266
+ caret.style.transform = `translateY(${y}px)`;
214
267
  lineCounter.style.transform = `translateY(${y}px)`;
215
268
  };
216
269
  editor1.addEventListener("scroll", scroll);
@@ -253,7 +306,7 @@ async function createEditor(editor: HTMLElement, data: { value?: string, languag
253
306
  l = "xml";
254
307
  }
255
308
  const mod = await import(`https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/languages/${l}.js`);
256
-
309
+ languages.registerLanguage(l, mod)
257
310
  }
258
311
  language = l;
259
312
  refresh();
@@ -263,10 +316,88 @@ async function createEditor(editor: HTMLElement, data: { value?: string, languag
263
316
  setValue,
264
317
  focus,
265
318
  setLanguage,
266
- destroy
319
+ destroy,
320
+ onDidChangeModelContent,
267
321
  };
268
322
  }
269
323
 
324
+ function wrapCode(code: string, wrapAt = 71) {
325
+ return code.split("\n").map(line => {
326
+ if (line.length <= wrapAt) return line;
327
+
328
+ const indent = line.match(/^(\s*)/)?.at(1) ?? "";
329
+ const words = line.trimStart().split(" ");
330
+ const wrappedLines = [];
331
+ let currentLine = indent; // first line keeps indent
332
+ let isFirstLine = true;
333
+
334
+ for (const word of words) {
335
+ if (word.length > wrapAt) {
336
+ if (currentLine.trim()) {
337
+ wrappedLines.push(currentLine);
338
+ currentLine = "";
339
+ isFirstLine = false;
340
+ }
341
+ for (let i = 0; i < word.length; i += wrapAt) {
342
+ wrappedLines.push(word.slice(i, i + wrapAt));
343
+ }
344
+ continue;
345
+ }
346
+
347
+ const test = currentLine ? currentLine + " " + word : word;
348
+ if (test.length > wrapAt) {
349
+ wrappedLines.push(currentLine);
350
+ currentLine = word; // no indent on continuation lines
351
+ isFirstLine = false;
352
+ } else {
353
+ currentLine = test;
354
+ }
355
+ }
356
+
357
+ if (currentLine) wrappedLines.push(currentLine);
358
+ return wrappedLines.join("\n");
359
+ }).join("\n");
360
+ }
361
+
362
+ function getWrapMap(code: string, wrapAt = 71) {
363
+ const wrapMap: any = []; // wrapMap[originalLineIndex] = number of visual lines it produces
364
+
365
+ code.split("\n").forEach((line, i) => {
366
+ if (line.length <= wrapAt) {
367
+ wrapMap[i] = 1; // no wrap, 1 visual line
368
+ return;
369
+ }
370
+
371
+ const indent = line.match(/^(\s*)/)?.at(1) ?? "";
372
+ const words = line.trimStart().split(" ");
373
+ let currentLine = indent;
374
+ let visualLines = 1;
375
+
376
+ for (const word of words) {
377
+ if (word.length > wrapAt) {
378
+ if (currentLine.trim()) {
379
+ visualLines++;
380
+ currentLine = "";
381
+ }
382
+ visualLines += Math.floor(word.length / wrapAt);
383
+ continue;
384
+ }
385
+
386
+ const test = currentLine ? currentLine + " " + word : word;
387
+ if (test.length > wrapAt) {
388
+ visualLines++;
389
+ currentLine = word;
390
+ } else {
391
+ currentLine = test;
392
+ }
393
+ }
394
+
395
+ wrapMap[i] = visualLines;
396
+ });
397
+
398
+ return wrapMap;
399
+ }
400
+
270
401
  function escapeHtml(str: string) {
271
402
  return str
272
403
  .replace(/&/g, "&amp;")
@@ -274,12 +405,11 @@ function escapeHtml(str: string) {
274
405
  .replace(/>/g, "&gt;");
275
406
  }
276
407
 
277
- async function _render(code: string, language: string, editor: HTMLElement) {
408
+ async function _render(code: string, language: any, editor: HTMLElement) {
278
409
  // If no editor context provided, just highlight everything (initial load)
279
410
  if (!editor) {
280
411
  return hljs.highlight(code, { language }).value;
281
412
  }
282
-
283
413
  const scrollTop = editor.scrollTop;
284
414
  const scrollBottom = scrollTop + editor.clientHeight;
285
415
  const style = getComputedStyle(editor);
@@ -298,12 +428,18 @@ async function _render(code: string, language: string, editor: HTMLElement) {
298
428
 
299
429
  // Split into three sections
300
430
  const beforeLines = lines.slice(0, visibleStart);
301
- const visibleLines = lines.slice(visibleStart, visibleEnd);
431
+ const visibleLines: any = lines.slice(visibleStart, visibleEnd);
302
432
  const afterLines = lines.slice(visibleEnd);
303
433
 
304
434
  // Only highlight visible portion
305
-
306
- const highlightedVisible = hljs.highlight(visibleLines.join("\n"), { language }).value;
435
+ let wrappedCode;
436
+ if (visibleLines.length === 0) {
437
+ wrappedCode = wrapCode(code, 68);
438
+ }
439
+ else {
440
+ wrappedCode = wrapCode(visibleLines.join("\n"), 71)
441
+ }
442
+ const highlightedVisible = hljs.highlight(wrappedCode, { language }).value;
307
443
  // Plain text for non-visible areas (no highlighting = faster)
308
444
  if (highlightedVisible.trim() === "") {
309
445
  return hljs.highlight(escapeHtml(code), { language }).value;