@pfmcodes/caret 0.2.0 → 0.2.5

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/esm/editor.js CHANGED
@@ -2,6 +2,7 @@ import hljs from "https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/core.js"; // Us
2
2
  import languages from "./languages.js";
3
3
 
4
4
  languages.init();
5
+ let numberWrappedCode = 0;
5
6
 
6
7
  async function createEditor(editor, data) {
7
8
  const editor1 = document.createElement("textarea");
@@ -14,18 +15,27 @@ async function createEditor(editor, data) {
14
15
  const lineColor = isDark ? "#fff" : "#000";
15
16
  const lineCounter = document.createElement("div");
16
17
 
17
- editor1.id = "Caret-textarea";
18
- highlighted.id = "Caret-highlighted";
19
- caret.id = "Caret-caret";
20
- lineCounter.id = "Caret-lineCounter";
21
- editor1.className = 'dark';
22
- highlighted.className = 'dark';
23
- caret.className = 'dark';
24
- lineCounter.className = 'dark';
18
+ const i = Math.random().toString(16).slice(2);
19
+ editor1.id = `Caret-textarea-${i}`;
20
+ highlighted.id = `Caret-highlighted-${i}`;
21
+ caret.id = `Caret-caret-${i}`;
22
+ lineCounter.id = `Caret-lineCounter-${i}`;
23
+ editor1.classList.add("Caret-textarea");
24
+ highlighted.classList.add("Caret-highlighted");
25
+ caret.classList.add("Caret-caret");
26
+ lineCounter.classList.add("Caret-lineCounter");
27
+ editor1.classList.add("dark");
28
+ highlighted.classList.add("dark");
29
+ caret.classList.add("dark");
30
+ lineCounter.classList.add("dark");
25
31
  editor1.style.backgroundColor = isDark ? "#222" : "#fff";
26
32
  let code = data.value || "";
27
33
  let language = data.language;
28
34
  let theme = data.theme;
35
+ let lock = data.readOnly || false;
36
+ if (lock) {
37
+ editor1.readOnly = true;
38
+ }
29
39
  if (!languages.registeredLanguages.includes(language)) {
30
40
  const mod = await import(`https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/languages/${language}.js`);
31
41
  languages.registerLanguage(language, mod.default);
@@ -61,7 +71,6 @@ async function createEditor(editor, data) {
61
71
  editor.style = "position: relative; width: 600px; height: 300px; overflow: hidden; /* 👈 CRITICAL */ font-size: 14px;"
62
72
  if (code && editor && editor1 && language && highlighted) {
63
73
  editor1.style.paddingTop = "-9px";
64
- console.log(data.value + " data.value");
65
74
  editor1.value = data.value;
66
75
  highlighted.innerHTML = await _render(data.value, language, editor1);
67
76
  }
@@ -134,12 +143,15 @@ async function createEditor(editor, data) {
134
143
  }
135
144
 
136
145
  function updateLineNumbers() {
137
- const lineCount = editor1.value.split("\n").length;
146
+ const lines = editor1.value.split("\n");
147
+ const wrapMap = getWrapMap(editor1.value, 71);
138
148
 
139
149
  let html = "";
140
- for (let i = 1; i <= lineCount; i++) {
141
- html += `<div class="Caret-lineCounter-number" style="color: ${lineColor}">${i}</div>`;
142
- }
150
+ lines.forEach((line, i) => {
151
+ const visualLines = wrapMap[i] || 1;
152
+ // first visual line gets the number
153
+ html += `<div class="Caret-lineCounter-number" style="color: ${lineColor}; height: calc(1.5em * ${visualLines})">${i + 1}</div>`;
154
+ });
143
155
 
144
156
  lineCounter.innerHTML = html;
145
157
  }
@@ -167,47 +179,89 @@ async function createEditor(editor, data) {
167
179
 
168
180
  editor1.addEventListener("blur", blur);
169
181
 
182
+ function getVisualRow(text, wrapAt) {
183
+ // simulate exactly what wrapCode does, but track caret position
184
+ const words = text.split(" ");
185
+ let currentLine = "";
186
+ let visualRow = 0;
187
+
188
+ for (let w = 0; w < words.length; w++) {
189
+ const word = words[w];
190
+ const isLast = w === words.length - 1;
191
+ const test = currentLine ? currentLine + " " + word : word;
192
+
193
+ if (test.length > wrapAt && currentLine !== "") {
194
+ visualRow++;
195
+ currentLine = word;
196
+ } else {
197
+ currentLine = test;
198
+ }
199
+ }
200
+
201
+ return { row: visualRow, lineText: currentLine };
202
+ }
203
+
170
204
  function updateCaret() {
171
205
  const start = editor1.selectionStart;
172
206
  const text = editor1.value.slice(0, start);
173
207
 
174
208
  const lines = text.split("\n");
175
209
  const lineIndex = lines.length - 1;
176
- const lineText = lines[lineIndex].replace(/\t/g, " ");
210
+ const currentLineText = lines[lineIndex].replace(/\t/g, " ");
177
211
 
178
212
  const style = getComputedStyle(editor1);
179
213
  const paddingLeft = parseFloat(style.paddingLeft);
180
214
  const paddingTop = parseFloat(style.paddingTop);
181
- const lineHeight = parseFloat(style.lineHeight);
215
+ const lineHeight = parseFloat(style.lineHeight) || 20;
182
216
 
183
217
  updateFontMetrics();
184
- const metrics = measureCtx.measureText("Mg");
185
- 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;
186
232
 
187
- caret.style.left =
188
- paddingLeft + measureCtx.measureText(lineText).width + "px";
189
- caret.style.top =
190
- -9 +
191
- paddingTop +
192
- lineIndex * lineHeight +
193
- (lineHeight - ascent) +
194
- "px";
233
+ totalVisualRows += row;
195
234
 
235
+ if (onWrappedLine) {
236
+ // on wrapped line - X is just the text on this visual row
237
+ caret.style.left = paddingLeft + measureCtx.measureText(lineText).width + "px";
238
+ } else {
239
+ // on original line - X is full line text width
240
+ caret.style.left = paddingLeft + measureCtx.measureText(lineText.trimEnd()).width + "px";
241
+ }
242
+
243
+ caret.style.top = -9 + paddingTop + totalVisualRows * lineHeight + (lineHeight - ascent) + "px";
196
244
  caret.style.height = `${lineHeight - 5}px`;
197
245
  }
246
+
198
247
  const input = async () => {
199
248
  caret.style.opacity = "1";
200
249
  highlighted.innerHTML = await _render(editor1.value, language, editor1);
201
250
  updateLineNumbers();
202
251
  updateCaret();
203
252
  };
253
+
254
+ const onDidChangeModelContent = (fn) => {
255
+ editor1.addEventListener("input", fn)
256
+ }
257
+
204
258
  editor1.addEventListener("input", input);
205
259
  const scroll = async () => {
206
260
  const x = -editor1.scrollLeft;
207
261
  const y = -editor1.scrollTop;
208
262
  highlighted.innerHTML = await _render(editor1.value, language, editor1);
209
- highlighted.style.transform = `translate(${x}px, ${y}px)`;
210
- caret.style.transform = `translate(${x}px, ${y}px)`;
263
+ highlighted.style.transform = `translateY(${y}px)`;
264
+ caret.style.transform = `translateY(${y}px)`;
211
265
  lineCounter.style.transform = `translateY(${y}px)`;
212
266
  };
213
267
  editor1.addEventListener("scroll", scroll);
@@ -244,13 +298,13 @@ async function createEditor(editor, data) {
244
298
  refresh();
245
299
  }
246
300
  async function setLanguage(l) {
301
+ if (l === "html" || l === "svg") {
302
+ language = "xml";
303
+ l = "xml";
304
+ }
247
305
  if (!languages.registeredLanguages.includes(l)) {
248
- if (l === "html" || l === "svg") {
249
- language = "xml";
250
- l = "xml";
251
- }
252
306
  const mod = await import(`https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/languages/${l}.js`);
253
-
307
+ languages.registerLanguage(l, mod)
254
308
  }
255
309
  language = l;
256
310
  refresh();
@@ -260,10 +314,88 @@ async function createEditor(editor, data) {
260
314
  setValue,
261
315
  focus,
262
316
  setLanguage,
263
- destroy
317
+ destroy,
318
+ onDidChangeModelContent,
264
319
  };
265
320
  }
266
321
 
322
+ function wrapCode(code, wrapAt = 71) {
323
+ return code.split("\n").map(line => {
324
+ if (line.length <= wrapAt) return line;
325
+
326
+ const indent = line.match(/^(\s*)/)[1];
327
+ const words = line.trimStart().split(" ");
328
+ const wrappedLines = [];
329
+ let currentLine = indent; // first line keeps indent
330
+ let isFirstLine = true;
331
+
332
+ for (const word of words) {
333
+ if (word.length > wrapAt) {
334
+ if (currentLine.trim()) {
335
+ wrappedLines.push(currentLine);
336
+ currentLine = "";
337
+ isFirstLine = false;
338
+ }
339
+ for (let i = 0; i < word.length; i += wrapAt) {
340
+ wrappedLines.push(word.slice(i, i + wrapAt));
341
+ }
342
+ continue;
343
+ }
344
+
345
+ const test = currentLine ? currentLine + " " + word : word;
346
+ if (test.length > wrapAt) {
347
+ wrappedLines.push(currentLine);
348
+ currentLine = word; // no indent on continuation lines
349
+ isFirstLine = false;
350
+ } else {
351
+ currentLine = test;
352
+ }
353
+ }
354
+
355
+ if (currentLine) wrappedLines.push(currentLine);
356
+ return wrappedLines.join("\n");
357
+ }).join("\n");
358
+ }
359
+
360
+ function getWrapMap(code, wrapAt = 71) {
361
+ const wrapMap = []; // wrapMap[originalLineIndex] = number of visual lines it produces
362
+
363
+ code.split("\n").forEach((line, i) => {
364
+ if (line.length <= wrapAt) {
365
+ wrapMap[i] = 1; // no wrap, 1 visual line
366
+ return;
367
+ }
368
+
369
+ const indent = line.match(/^(\s*)/)[1];
370
+ const words = line.trimStart().split(" ");
371
+ let currentLine = indent;
372
+ let visualLines = 1;
373
+
374
+ for (const word of words) {
375
+ if (word.length > wrapAt) {
376
+ if (currentLine.trim()) {
377
+ visualLines++;
378
+ currentLine = "";
379
+ }
380
+ visualLines += Math.floor(word.length / wrapAt);
381
+ continue;
382
+ }
383
+
384
+ const test = currentLine ? currentLine + " " + word : word;
385
+ if (test.length > wrapAt) {
386
+ visualLines++;
387
+ currentLine = word;
388
+ } else {
389
+ currentLine = test;
390
+ }
391
+ }
392
+
393
+ wrapMap[i] = visualLines;
394
+ });
395
+
396
+ return wrapMap;
397
+ }
398
+
267
399
  function escapeHtml(str) {
268
400
  return str
269
401
  .replace(/&/g, "&amp;")
@@ -276,7 +408,6 @@ async function _render(code, language, editor) {
276
408
  if (!editor) {
277
409
  return hljs.highlight(code, { language }).value;
278
410
  }
279
-
280
411
  const scrollTop = editor.scrollTop;
281
412
  const scrollBottom = scrollTop + editor.clientHeight;
282
413
  const style = getComputedStyle(editor);
@@ -299,8 +430,14 @@ async function _render(code, language, editor) {
299
430
  const afterLines = lines.slice(visibleEnd);
300
431
 
301
432
  // Only highlight visible portion
302
-
303
- const highlightedVisible = hljs.highlight(visibleLines.join("\n"), { language }).value;
433
+ let wrappedCode;
434
+ if (visibleLines == []) {
435
+ wrappedCode = wrapCode(code, 68);
436
+ }
437
+ else {
438
+ wrappedCode = wrapCode(visibleLines.join("\n"), 71)
439
+ }
440
+ const highlightedVisible = hljs.highlight(wrappedCode, { language }).value;
304
441
  // Plain text for non-visible areas (no highlighting = faster)
305
442
  if (highlightedVisible.trim() === "") {
306
443
  return hljs.highlight(escapeHtml(code), { language }).value;
package/esm/languages.js CHANGED
@@ -24,8 +24,13 @@ function init() {
24
24
  // Register all languages
25
25
  hljs.registerLanguage("javascript", javascript);
26
26
  hljs.registerLanguage("xml", xml);
27
+ hljs.registerLanguage("typescript", typescript);
28
+ hljs.registerAliases(["jsx"], { languageName: "javascript" });
29
+ hljs.registerAliases(["js"], { languageName: "javascript" });
30
+ hljs.registerAliases(["ts"], { languageName: "typescript" });
31
+ hljs.registerAliases(["html"], { languageName: "xml" });
32
+ hljs.registerAliases(["svg"], { languageName: "xml" });
27
33
  hljs.registerLanguage("css", css);
28
- hljs.registerLanguage("html", xml);
29
34
  hljs.registerLanguage("python", python);
30
35
  hljs.registerLanguage("java", java);
31
36
  hljs.registerLanguage("csharp", csharp);
@@ -37,18 +42,15 @@ function init() {
37
42
  hljs.registerLanguage("rust", rust);
38
43
  hljs.registerLanguage("kotlin", kotlin);
39
44
  hljs.registerLanguage("swift", swift);
40
- hljs.registerLanguage("typescript", typescript);
41
45
  hljs.registerLanguage("json", json);
42
46
  hljs.registerLanguage("bash", bash);
43
47
  hljs.registerLanguage("shell", bash);
44
48
  hljs.registerLanguage("sh", bash);
45
49
  hljs.registerLanguage("plaintext", plaintext);
46
- registeredLanguages = [
47
- "javascript",
48
- "js",
49
- "xml",
50
- "html",
51
- "svg",
50
+ registeredLanguages.push(
51
+ "javascript", "js",
52
+ "xml", "html", "svg",
53
+ "css",
52
54
  "python",
53
55
  "java",
54
56
  "csharp",
@@ -62,11 +64,9 @@ function init() {
62
64
  "swift",
63
65
  "typescript",
64
66
  "json",
65
- "bash",
66
- "shell",
67
- "sh",
67
+ "bash", "shell", "sh",
68
68
  "plaintext"
69
- ]
69
+ );
70
70
  }
71
71
 
72
72
  function registerLanguage(name, definition) {
package/index.css CHANGED
@@ -1,4 +1,4 @@
1
- #Caret-textarea {
1
+ .Caret-textarea {
2
2
  position: absolute;
3
3
  border: none;
4
4
  inset: 0;
@@ -7,33 +7,40 @@
7
7
  background: transparent;
8
8
  color: transparent;
9
9
  caret-color: #fff;
10
- border: 1px solid #ccc;
11
10
  overflow-x: scroll; /* 👈 ONLY THIS SCROLLS */
12
11
  z-index: 1;
13
- width: 100%;
12
+ width: auto;
13
+ min-width: max-content;
14
14
  }
15
15
 
16
- #Caret-texarea::-webkit-scrollbar {
17
- z-index: 3;
16
+ ::-webkit-scrollbar {
17
+ display: none;
18
+ }
19
+
20
+ .Caret-caret, .Caret-lineCounter, .Caret-textarea {
21
+ overflow: auto;
22
+ overflow-x: auto;
18
23
  }
19
24
 
20
- #Caret-textarea:focus {
25
+ .Caret-textarea:focus {
21
26
  outline: none;
22
27
  }
23
28
 
24
- #Caret-highlighted {
29
+ .Caret-highlighted {
25
30
  position: absolute;
26
31
  top: 0;
27
32
  left: 0;
28
33
  margin: 0;
29
34
  padding: 10px;
30
35
  pointer-events: none;
31
- overflow: hidden; /* 👈 IMPORTANT */
36
+ overflow: visible; /* 👈 IMPORTANT */
32
37
  z-index: 2;
33
38
  white-space: pre;
39
+ min-width: max-content;
40
+ width: auto;
34
41
  }
35
42
 
36
- #Caret-caret {
43
+ .Caret-caret {
37
44
  position: absolute;
38
45
  width: 2px !important; /* 👈 thickness */
39
46
  opacity: 0;
@@ -44,20 +51,20 @@
44
51
  animation: blink 1s step-end infinite;
45
52
  }
46
53
 
47
- #Caret-textarea,
48
- #Caret-highlighted,
49
- #Caret-lineCounter {
54
+ .Caret-textarea,
55
+ .Caret-highlighted,
56
+ .Caret-lineCounter {
50
57
  font: inherit;
51
58
  line-height: inherit;
52
59
  }
53
60
 
54
- #Caret-textarea::selection {
61
+ .Caret-textarea::selection {
55
62
  color: transparent;
56
63
  background-color: rgb(79, 79, 249);
57
64
  }
58
65
 
59
- #Caret-textarea,
60
- #Caret-highlighted {
66
+ .Caret-textarea,
67
+ .Caret-highlighted {
61
68
  font-family: monospace;
62
69
  font-size: 14px;
63
70
  line-height: 1.5;
@@ -65,7 +72,7 @@
65
72
  tab-size: 2;
66
73
  }
67
74
 
68
- #Caret-lineCounter {
75
+ .Caret-lineCounter {
69
76
  position: absolute;
70
77
  top: 0;
71
78
  left: 0;
@@ -74,7 +81,7 @@
74
81
  font-family: monospace;
75
82
  font: 18px;
76
83
  line-height: 1.5;
77
- color: #999;
84
+ color: .999;
78
85
  text-align: right;
79
86
  user-select: none;
80
87
  opacity: 0.7;
@@ -92,12 +99,12 @@
92
99
  50% { opacity: 0; }
93
100
  }
94
101
 
95
- #Caret-caret.dark {
102
+ .Caret-caret.dark {
96
103
  background: #fff0; /* caret color */
97
104
  }
98
- #Caret-highlighted.dark {
105
+ .Caret-highlighted.dark {
99
106
  color: #fff;
100
107
  }
101
- #Caret-lineCounter.dark {
108
+ .Caret-lineCounter.dark {
102
109
  color: #fff;
103
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pfmcodes/caret",
3
- "version": "0.2.0",
3
+ "version": "0.2.5",
4
4
  "description": "The official code editor engine for lexius",
5
5
  "type": "module",
6
6
  "main": "./esm/index.js",