@pfmcodes/caret 0.2.1 → 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/README.md +25 -35
- package/commonjs/editor.js +169 -32
- package/commonjs/languages.js +26 -21
- package/esm/editor.js +173 -36
- package/esm/languages.js +12 -12
- package/index.css +27 -20
- package/package.json +1 -1
- package/types/editor.ts +189 -53
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
146
|
+
const lines = editor1.value.split("\n");
|
|
147
|
+
const wrapMap = getWrapMap(editor1.value, 71);
|
|
138
148
|
|
|
139
149
|
let html = "";
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
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
|
|
185
|
-
|
|
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
|
-
|
|
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 = `
|
|
210
|
-
caret.style.transform = `
|
|
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, "&")
|
|
@@ -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
|
-
|
|
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
|
-
"
|
|
49
|
-
"
|
|
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
|
-
|
|
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:
|
|
12
|
+
width: auto;
|
|
13
|
+
min-width: max-content;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
25
|
+
.Caret-textarea:focus {
|
|
21
26
|
outline: none;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
.Caret-textarea,
|
|
55
|
+
.Caret-highlighted,
|
|
56
|
+
.Caret-lineCounter {
|
|
50
57
|
font: inherit;
|
|
51
58
|
line-height: inherit;
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
|
|
61
|
+
.Caret-textarea::selection {
|
|
55
62
|
color: transparent;
|
|
56
63
|
background-color: rgb(79, 79, 249);
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
102
|
+
.Caret-caret.dark {
|
|
96
103
|
background: #fff0; /* caret color */
|
|
97
104
|
}
|
|
98
|
-
|
|
105
|
+
.Caret-highlighted.dark {
|
|
99
106
|
color: #fff;
|
|
100
107
|
}
|
|
101
|
-
|
|
108
|
+
.Caret-lineCounter.dark {
|
|
102
109
|
color: #fff;
|
|
103
110
|
}
|