@pfmcodes/caret 0.2.9 → 0.3.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/editor.js DELETED
@@ -1,470 +0,0 @@
1
- import hljs from "https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/core.js"; // Use default export
2
- import languages from "./langauges.js";
3
-
4
- languages.init();
5
- let numberWrappedCode = 0;
6
-
7
- async function createEditor(editor, data) {
8
- const editor1 = document.createElement("textarea");
9
- const highlighted = document.createElement("pre");
10
- const caret = document.createElement("div");
11
- const measureCanvas = document.createElement("canvas");
12
- const measureCtx = measureCanvas.getContext("2d");
13
- const isDark = data.theme && (data.theme.includes("dark") || data.theme.includes("night"));
14
- const caretColor = isDark ? "#fff" : "#7116d8";
15
- const lineColor = isDark ? "#fff" : "#000";
16
- const lineCounter = document.createElement("div");
17
-
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");
31
- editor1.style.backgroundColor = isDark ? "#222" : "#fff";
32
- let code = data.value || "";
33
- let language = data.language;
34
- let theme = data.theme;
35
- let lock = data.readOnly || false;
36
- if (lock) {
37
- editor1.readOnly = true;
38
- }
39
- if (!languages.registeredLanguages.includes(language)) {
40
- const mod = await import(`https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/languages/${language}.js`);
41
- languages.registerLanguage(language, mod.default);
42
- languages.registeredLanguages.push(language);
43
- }
44
- if (theme) {
45
- let themeLink = document.getElementById("Caret-theme")
46
- if (!themeLink) {
47
- const link = document.createElement("link");
48
- link.rel = "stylesheet";
49
- link.id = "Caret-theme";
50
- link.href = `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/${theme}.css`;
51
- document.head.appendChild(link);
52
- } else {
53
- themeLink.href = `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/${theme}.css`;
54
- }
55
- } else {
56
- let themeLink = document.getElementById("Caret-theme");
57
- if (!themeLink) {
58
- const link = document.createElement("link");
59
- link.rel = "stylesheet";
60
- link.id = "Caret-theme";
61
- link.href = `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/hybrid.css`;
62
- document.head.appendChild(link);
63
- } else {
64
- themeLink.href = `./highlight.js/styles/hybrid.css`;
65
- }
66
- }
67
- editor1.spellcheck = false;
68
- editor1.autocapitalize = "off";
69
- editor1.autocomplete = "off";
70
- editor1.autocorrect = "off";
71
- editor.style = "position: relative; width: 600px; height: 300px; overflow: hidden; /* 👈 CRITICAL */ font-size: 14px;"
72
- if (code && editor && editor1 && language && highlighted) {
73
- editor1.style.paddingTop = "-9px";
74
- editor1.value = code;
75
- let h = await _render(code, language, editor1);
76
- highlighted.innerHTML = h;
77
- console.log({h: h, code: code})
78
- }
79
- const keyDown = async (e) => {
80
- if (e.key !== "Tab") return;
81
-
82
- e.preventDefault();
83
-
84
- const value = editor1.value;
85
- const start = editor1.selectionStart;
86
- const end = editor1.selectionEnd;
87
-
88
- const indent = " ";
89
-
90
- // Find line start & end
91
- const lineStart = value.lastIndexOf("\n", start - 1) + 1;
92
- const lineEnd = value.indexOf("\n", end);
93
- const finalLineEnd = lineEnd === -1 ? value.length : lineEnd;
94
-
95
- const block = value.slice(lineStart, finalLineEnd);
96
- const lines = block.split("\n");
97
-
98
- let newLines;
99
- let delta = 0;
100
-
101
- if (e.shiftKey) {
102
- // UNINDENT
103
- newLines = lines.map(line => {
104
- if (line.startsWith(indent)) {
105
- delta -= indent.length;
106
- return line.slice(indent.length);
107
- }
108
- if (line.startsWith("\t")) {
109
- delta -= 1;
110
- return line.slice(1);
111
- }
112
- return line;
113
- });
114
- } else {
115
- // INDENT
116
- newLines = lines.map(line => indent + line);
117
- delta = indent.length;
118
- }
119
-
120
- const newBlock = newLines.join("\n");
121
-
122
- editor1.value =
123
- value.slice(0, lineStart) +
124
- newBlock +
125
- value.slice(finalLineEnd);
126
-
127
- // Fix selection
128
- editor1.selectionStart = start + delta;
129
- editor1.selectionEnd =
130
- end + delta * newLines.length;
131
-
132
- highlighted.innerHTML = await _render(editor1.value, language, editor1);
133
- updateLineNumbers();
134
- updateCaret();
135
- }
136
- editor1.addEventListener("keydown", keyDown);
137
- editor.appendChild(lineCounter);
138
- editor.appendChild(highlighted);
139
- editor.appendChild(editor1);
140
- editor.appendChild(caret);
141
-
142
- function updateFontMetrics() {
143
- const style = getComputedStyle(editor1);
144
- measureCtx.font = `${style.fontSize} ${style.fontFamily}`;
145
- }
146
-
147
- function updateLineNumbers() {
148
- const lines = editor1.value.split("\n");
149
- const wrapMap = getWrapMap(editor1.value, 71);
150
-
151
- let html = "";
152
- lines.forEach((line, i) => {
153
- const visualLines = wrapMap[i] || 1;
154
- // first visual line gets the number
155
- html += `<div class="Caret-lineCounter-number" style="color: ${lineColor}; height: calc(1.5em * ${visualLines})">${i + 1}</div>`;
156
- });
157
-
158
- lineCounter.innerHTML = html;
159
- }
160
-
161
- highlighted.style.paddingTop = "12px"
162
-
163
- function getFontMetrics() {
164
- const metrics = measureCtx.measureText("Mg");
165
- return {
166
- ascent: metrics.actualBoundingBoxAscent,
167
- descent: metrics.actualBoundingBoxDescent,
168
- height: metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent
169
- };
170
- }
171
- const focus = () => {
172
- caret.style.opacity = "1";
173
- caret.style.background = caretColor;
174
- };
175
-
176
- editor1.addEventListener("focus", focus);
177
-
178
- const blur = () => {
179
- caret.style.opacity = "0";
180
- };
181
-
182
- editor1.addEventListener("blur", blur);
183
-
184
- function getVisualRow(text, wrapAt) {
185
- // simulate exactly what wrapCode does, but track caret position
186
- const words = text.split(" ");
187
- let currentLine = "";
188
- let visualRow = 0;
189
-
190
- for (let w = 0; w < words.length; w++) {
191
- const word = words[w];
192
- const isLast = w === words.length - 1;
193
- const test = currentLine ? currentLine + " " + word : word;
194
-
195
- if (test.length > wrapAt && currentLine !== "") {
196
- visualRow++;
197
- currentLine = word;
198
- } else {
199
- currentLine = test;
200
- }
201
- }
202
-
203
- return { row: visualRow, lineText: currentLine };
204
- }
205
-
206
- function updateCaret() {
207
- const start = editor1.selectionStart;
208
- const text = editor1.value.slice(0, start);
209
-
210
- const lines = text.split("\n");
211
- const lineIndex = lines.length - 1;
212
- const currentLineText = lines[lineIndex].replace(/\t/g, " ");
213
-
214
- const style = getComputedStyle(editor1);
215
- const paddingLeft = parseFloat(style.paddingLeft);
216
- const paddingTop = parseFloat(style.paddingTop);
217
- const lineHeight = parseFloat(style.lineHeight) || 20;
218
-
219
- updateFontMetrics();
220
- const ascent = measureCtx.measureText("Mg").actualBoundingBoxAscent;
221
-
222
- const WRAP_AT = 71;
223
-
224
- // count visual rows from all previous lines
225
- let totalVisualRows = 0;
226
- for (let i = 0; i < lineIndex; i++) {
227
- const l = lines[i].replace(/\t/g, " ");
228
- const { row } = getVisualRow(l, WRAP_AT);
229
- totalVisualRows += row + 1;
230
- }
231
-
232
- const { row, lineText } = getVisualRow(currentLineText, WRAP_AT);
233
- const onWrappedLine = row > 0;
234
-
235
- totalVisualRows += row;
236
-
237
- if (onWrappedLine) {
238
- // on wrapped line - X is just the text on this visual row
239
- caret.style.left = paddingLeft + measureCtx.measureText(lineText).width + "px";
240
- } else {
241
- // on original line - X is full line text width
242
- caret.style.left = paddingLeft + measureCtx.measureText(lineText.trimEnd()).width + "px";
243
- }
244
-
245
- caret.style.top = -9 + paddingTop + totalVisualRows * lineHeight + (lineHeight - ascent) + "px";
246
- caret.style.height = `${lineHeight - 5}px`;
247
- }
248
-
249
- const input = async () => {
250
- caret.style.opacity = "1";
251
- highlighted.innerHTML = await _render(editor1.value, language, editor1);
252
- updateLineNumbers();
253
- updateCaret();
254
- };
255
-
256
- const onDidChangeModelContent = (fn) => {
257
- editor1.addEventListener("input", fn)
258
- }
259
-
260
- editor1.addEventListener("input", input);
261
- const scroll = async () => {
262
- const x = -editor1.scrollLeft;
263
- const y = -editor1.scrollTop;
264
- highlighted.innerHTML = await _render(editor1.value, language, editor1);
265
- highlighted.style.transform = `translateY(${y}px)`;
266
- caret.style.transform = `translateY(${y}px)`;
267
- lineCounter.style.transform = `translateY(${y}px)`;
268
- };
269
- editor1.addEventListener("scroll", scroll);
270
-
271
- updateFontMetrics();
272
- getFontMetrics();
273
-
274
- editor1.addEventListener("click", updateCaret);
275
- editor1.addEventListener("keyup", updateCaret);
276
-
277
- // Initial caret position
278
- updateLineNumbers();
279
- updateCaret();
280
-
281
- // Focus the editor
282
- editor1.focus();
283
- function destroy() {
284
- editor1.removeEventListener('click', updateCaret);
285
- editor1.removeEventListener('keyup', updateCaret);
286
- editor1.removeEventListener('scroll', scroll);
287
- editor1.removeEventListener('keydown', keyDown);
288
- editor.innerHTML = "";
289
- }
290
- async function refresh() {
291
- highlighted.innerHTML = await _render(editor1.value, language, editor1);
292
- updateLineNumbers();
293
- updateCaret();
294
- }
295
- function getValue() {
296
- return editor1.value;
297
- }
298
- function setValue(i) {
299
- editor1.value = i;
300
- refresh();
301
- }
302
- async function setLanguage(l) {
303
- if (l === "html" || l === "svg") {
304
- language = "xml";
305
- l = "xml";
306
- }
307
- if (!languages.registeredLanguages.includes(l)) {
308
- const mod = await import(`https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/languages/${l}.js`);
309
- languages.registerLanguage(l, mod)
310
- }
311
- language = l;
312
- refresh();
313
- }
314
- return {
315
- getValue,
316
- setValue,
317
- focus,
318
- setLanguage,
319
- destroy,
320
- onDidChangeModelContent,
321
- };
322
- }
323
-
324
- function wrapCode(code, wrapAt = 71) {
325
- return code.split("\n").map(line => {
326
- if (line.length <= wrapAt) return line;
327
-
328
- const indent = line.match(/^(\s*)/)[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, wrapAt = 71) {
363
- const wrapMap = []; // 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*)/)[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
-
401
- async function _render(code, language, editor) {
402
- // If no editor context provided, just highlight everything (initial load)
403
- if (!editor) {
404
- return hljs.highlight(code, { language }).value;
405
- }
406
- const scrollTop = editor.scrollTop;
407
- const scrollBottom = scrollTop + editor.clientHeight;
408
- const style = getComputedStyle(editor);
409
- const lineHeight = parseFloat(style.lineHeight);
410
-
411
- // Calculate visible line range
412
- const startLine = Math.floor(scrollTop / lineHeight);
413
- const endLine = Math.ceil(scrollBottom / lineHeight);
414
-
415
- const lines = code.split("\n");
416
-
417
- // Add buffer (render extra lines above/below for smooth scrolling)
418
- const bufferLines = 10;
419
- const visibleStart = Math.max(0, startLine - bufferLines) || 0;
420
- const visibleEnd = Math.min(lines.length, endLine + bufferLines) || 0;
421
-
422
- // Split into three sections
423
- const beforeLines = lines.slice(0, visibleStart);
424
- const visibleLines = lines.slice(visibleStart, visibleEnd);
425
- const afterLines = lines.slice(visibleEnd);
426
-
427
- // Only highlight visible portion
428
- let wrappedCode;
429
- if (visibleLines == []) {
430
- wrappedCode = wrapCode(code, 68);
431
- }
432
- else {
433
- wrappedCode = wrapCode(visibleLines.join("\n"), 71)
434
- }
435
- const highlightedVisible = hljs.highlight(wrappedCode, { language }).value;
436
- // Plain text for non-visible areas (no highlighting = faster)
437
- if (highlightedVisible.trim() === "") {
438
- return hljs.highlight(code, { language }).value;
439
- }
440
- const beforeHTML = "\n".repeat(beforeLines.length);
441
- const afterHTML = "\n".repeat(afterLines.length);
442
- return beforeHTML + highlightedVisible + afterHTML;
443
- }
444
-
445
- const editor = {
446
- createEditor
447
- };
448
-
449
- export default editor;
450
-
451
- /*
452
- createEditor(editor, data): creates the main editor using HTML elements like textarea, pre, div etc.
453
- Returns an object with the following methods:
454
- getValue() -> returns the current value from the editor
455
- setValue(val) -> sets a value to the editor
456
- focus() -> focuses the editor
457
- setLanguage(lang) -> changes the syntax highlighting language
458
- destroy() -> destroys the editor and removes all event listeners
459
- onDidChangeModelContent(fn) -> fires a callback whenever the editor content changes
460
-
461
- Internal functions:
462
- _render(code, language, editor) -> virtual renderer, only highlights visible lines for performance
463
- wrapCode(code, wrapAt) -> wraps long lines at word boundaries
464
- getWrapMap(code, wrapAt) -> returns an array mapping each line to its visual line count
465
- updateCaret() -> updates the caret position using canvas font metrics
466
- updateLineNumbers() -> re-renders line numbers, accounting for wrapped lines
467
- updateFontMetrics() -> updates the canvas font to match the textarea's computed style
468
- getFontMetrics() -> returns ascent, descent and height of the current font
469
- getVisualRow(text, wrapAt) -> returns which visual row and remaining text the caret is on
470
- */
package/index.css DELETED
@@ -1,110 +0,0 @@
1
- .Caret-textarea {
2
- position: absolute;
3
- border: none;
4
- inset: 0;
5
- padding: 10px 10px 10px 0px;
6
- font-family: monospace;
7
- background: transparent;
8
- color: transparent;
9
- caret-color: #fff;
10
- overflow-x: scroll; /* 👈 ONLY THIS SCROLLS */
11
- z-index: 1;
12
- width: auto;
13
- min-width: max-content;
14
- }
15
-
16
- ::-webkit-scrollbar {
17
- display: none;
18
- }
19
-
20
- .Caret-caret, .Caret-lineCounter, .Caret-textarea {
21
- overflow: auto;
22
- overflow-x: auto;
23
- }
24
-
25
- .Caret-textarea:focus {
26
- outline: none;
27
- }
28
-
29
- .Caret-highlighted {
30
- position: absolute;
31
- top: 0;
32
- left: 0;
33
- margin: 0;
34
- padding: 10px;
35
- pointer-events: none;
36
- overflow: visible; /* 👈 IMPORTANT */
37
- z-index: 2;
38
- white-space: pre;
39
- min-width: max-content;
40
- width: auto;
41
- }
42
-
43
- .Caret-caret {
44
- position: absolute;
45
- width: 2px !important; /* 👈 thickness */
46
- opacity: 0;
47
- display: none;
48
- pointer-events: none;
49
- z-index: 3;
50
- top: 12px;
51
- animation: blink 1s step-end infinite;
52
- }
53
-
54
- .Caret-textarea,
55
- .Caret-highlighted,
56
- .Caret-lineCounter {
57
- font: inherit;
58
- line-height: inherit;
59
- }
60
-
61
- .Caret-textarea::selection {
62
- color: transparent;
63
- background-color: rgb(79, 79, 249);
64
- }
65
-
66
- .Caret-textarea,
67
- .Caret-highlighted {
68
- font-family: monospace;
69
- font-size: 14px;
70
- line-height: 1.5;
71
- padding-left: 50px; /* 👈 space for line numbers */
72
- tab-size: 2;
73
- }
74
-
75
- .Caret-lineCounter {
76
- position: absolute;
77
- top: 0;
78
- left: 0;
79
- width: 40px;
80
- padding: 10px 5px;
81
- font-family: monospace;
82
- font: 18px;
83
- line-height: 1.5;
84
- color: .999;
85
- text-align: right;
86
- user-select: none;
87
- opacity: 0.7;
88
- letter-spacing: 0.5px;
89
- pointer-events: none;
90
- text-align: center;
91
- display: flex;
92
- flex-direction: column;
93
- height: fit-content;
94
- justify-content: space-around;
95
- z-index: 10;
96
- }
97
-
98
- @keyframes blink {
99
- 50% { opacity: 0; }
100
- }
101
-
102
- .Caret-caret.dark {
103
- background: #fff0; /* caret color */
104
- }
105
- .Caret-highlighted.dark {
106
- color: #fff;
107
- }
108
- .Caret-lineCounter.dark {
109
- color: #fff;
110
- }
package/theme.js DELETED
@@ -1,14 +0,0 @@
1
- function setTheme(name) {
2
- const link = document.getElementById("Caret-theme");
3
- link.href = `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/${name}.css`;
4
- }
5
-
6
- const theme = {
7
- setTheme
8
- }
9
-
10
- export default theme;
11
-
12
- /*
13
- setTheme() -> changes the current highlight.js theme
14
- */