@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/README.md +441 -386
- package/components/caret.js +95 -0
- package/components/font.js +16 -0
- package/components/lineCounter.js +91 -0
- package/components/textEditor.js +411 -0
- package/index.js +4 -19
- package/package.json +1 -1
- package/utilities.js +36 -0
- package/editor.js +0 -470
- package/index.css +0 -110
- package/theme.js +0 -14
- package/types/editor.ts +0 -481
- package/types/index.d.ts +0 -46
- package/types/index.ts +0 -22
- package/types/langauges.ts +0 -116
- package/types/theme.ts +0 -14
- /package/{langauges.js → components/languages.js} +0 -0
package/README.md
CHANGED
|
@@ -1,55 +1,68 @@
|
|
|
1
1
|

|
|
2
2
|
# Caret
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
|
-
[](https://github.com/PFMCODES/
|
|
5
|
-
[](https://github.com/
|
|
4
|
+
[](https://github.com/PFMCODES/Caret)
|
|
5
|
+
[](https://github.com/PFMCODES/Caret)
|
|
6
|
+
[](https://github.com/PFMCODES/Caret)
|
|
7
7
|
|
|
8
|
-
A lightweight,
|
|
8
|
+
A lightweight, fast code editor engine with real-time syntax highlighting, custom caret rendering, and a clean EditContext-based architecture. 551 lines. 26KB. 42ms load time.
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
12
|
+
- **EditContext API** - Clean input handling with no browser fighting, no textarea hacks
|
|
13
|
+
- **Live Syntax Highlighting** - Real-time highlighting powered by Highlight.js
|
|
14
|
+
- **Custom Caret** - Pixel-perfect caret positioning via Range API
|
|
14
15
|
- **Line Numbers** - Built-in line counter with dynamic updates
|
|
15
|
-
- **Smart Indentation** - Tab/Shift+Tab
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
16
|
+
- **Smart Indentation** - Tab/Shift+Tab for indenting and unindenting code blocks
|
|
17
|
+
- **Undo/Redo** - Full undo/redo stack with cursor position restoration (Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z)
|
|
18
|
+
- **Theme Support** - Custom background, text, caret and line counter colors
|
|
19
|
+
- **Lock Mode** - Read-only mode for displaying code
|
|
20
|
+
- **Font Support** - Custom font loading
|
|
21
|
+
- **Paste Handling** - Always pastes plain text, no rich HTML
|
|
22
|
+
- **ES Modules** - Modern ESM architecture
|
|
23
|
+
- **Lightweight** - 551 lines, 26KB total, loads in ~42ms
|
|
21
24
|
|
|
22
25
|
## Table of Contents
|
|
23
26
|
|
|
24
|
-
- [What's
|
|
27
|
+
- [What's New](#whats-new)
|
|
25
28
|
- [Installation](#installation)
|
|
26
29
|
- [Quick Start](#quick-start)
|
|
27
30
|
- [Project Structure](#project-structure)
|
|
28
31
|
- [Usage](#usage)
|
|
32
|
+
- [API Reference](#api-reference)
|
|
29
33
|
- [Customization](#customization)
|
|
30
|
-
- [
|
|
34
|
+
- [Browser Support](#browser-support)
|
|
35
|
+
- [Performance](#performance)
|
|
31
36
|
- [License](#license)
|
|
32
|
-
- [Perfomance Notes](#performance-notes)
|
|
33
37
|
|
|
34
|
-
## What's
|
|
38
|
+
## What's New
|
|
39
|
+
|
|
40
|
+
### v0.3.0 — Complete Rewrite
|
|
41
|
+
- **Ditched textarea** — rebuilt on Chrome's EditContext API
|
|
42
|
+
- **No more sync issues** — single text model, no dual layer fighting
|
|
43
|
+
- **Undo/redo with cursor restoration** — cursor returns to exact position
|
|
44
|
+
- **Pixel-perfect caret** — positioned via Range API, no canvas math
|
|
45
|
+
- **Modular architecture** — `textEditor.js`, `caret.js`, `lineCounter.js`, `font.js`, `languages.js`
|
|
46
|
+
- **setLanguage()** — switch language at runtime
|
|
47
|
+
- **delete()** — clean teardown with full event listener removal
|
|
48
|
+
- **onClick cursor positioning** — click anywhere to place cursor
|
|
49
|
+
- **Known limitation** — EditContext requires Chrome/Chromium (Firefox support pending)
|
|
35
50
|
|
|
36
51
|
### v0.2.8
|
|
37
|
-
-
|
|
52
|
+
- Bug fixes
|
|
38
53
|
|
|
39
54
|
### v0.2.7
|
|
40
|
-
-
|
|
55
|
+
- File optimizations and updated comments
|
|
41
56
|
|
|
42
57
|
### v0.2.6
|
|
43
|
-
|
|
44
|
-
- CommonJs support has been removed to reduce package size
|
|
58
|
+
- CommonJS support removed to reduce package size
|
|
45
59
|
|
|
46
60
|
### v0.2.5
|
|
47
61
|
- Multiple editor instances support
|
|
48
62
|
- Word-aware line wrapping
|
|
49
63
|
- Fixed language re-registration bug
|
|
50
|
-
- Known issue: custom caret may be 1 character off on original lines of a wrapped line
|
|
51
64
|
|
|
52
|
-
##
|
|
65
|
+
## Installation
|
|
53
66
|
|
|
54
67
|
### NPM
|
|
55
68
|
|
|
@@ -71,480 +84,522 @@ pnpm add @pfmcodes/caret
|
|
|
71
84
|
|
|
72
85
|
## Quick Start
|
|
73
86
|
|
|
74
|
-
### Basic Usage
|
|
75
|
-
|
|
76
87
|
```html
|
|
77
88
|
<!DOCTYPE html>
|
|
78
89
|
<html lang="en">
|
|
79
90
|
<head>
|
|
80
91
|
<meta charset="UTF-8">
|
|
81
92
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
82
|
-
<title>Caret</title>
|
|
83
|
-
<link rel="
|
|
93
|
+
<title>Caret Demo</title>
|
|
94
|
+
<link rel="shortcut icon" href="logo.svg" type="image/svg">
|
|
95
|
+
<style>
|
|
96
|
+
body {
|
|
97
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
98
|
+
padding: 20px;
|
|
99
|
+
background: #1a1a2e;
|
|
100
|
+
color: #fff;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
h1 {
|
|
104
|
+
text-align: center;
|
|
105
|
+
margin-bottom: 20px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#editor {
|
|
109
|
+
width: 900px;
|
|
110
|
+
height: 600px;
|
|
111
|
+
margin: 20px auto;
|
|
112
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
|
|
113
|
+
border-radius: 8px;
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#result {
|
|
118
|
+
width: 900px;
|
|
119
|
+
margin: 10px auto;
|
|
120
|
+
padding: 10px;
|
|
121
|
+
background: #111;
|
|
122
|
+
color: #0f0;
|
|
123
|
+
font-family: monospace;
|
|
124
|
+
font-size: 13px;
|
|
125
|
+
border-radius: 8px;
|
|
126
|
+
min-height: 40px;
|
|
127
|
+
white-space: pre-wrap;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.controls {
|
|
131
|
+
max-width: 900px;
|
|
132
|
+
margin: 0 auto 20px;
|
|
133
|
+
display: flex;
|
|
134
|
+
flex-wrap: wrap;
|
|
135
|
+
gap: 8px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.controls span {
|
|
139
|
+
font-size: 12px;
|
|
140
|
+
opacity: 0.5;
|
|
141
|
+
align-self: center;
|
|
142
|
+
margin: 0 4px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
button {
|
|
146
|
+
padding: 8px 16px;
|
|
147
|
+
border: none;
|
|
148
|
+
background: #7116d8;
|
|
149
|
+
color: white;
|
|
150
|
+
border-radius: 4px;
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
font-size: 13px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
button:hover {
|
|
156
|
+
background: #5a11ab;
|
|
157
|
+
}
|
|
158
|
+
</style>
|
|
84
159
|
</head>
|
|
85
160
|
<body>
|
|
161
|
+
<h1>Caret Demo</h1>
|
|
162
|
+
|
|
163
|
+
<div class="controls">
|
|
164
|
+
<span>Language:</span>
|
|
165
|
+
<button onclick="window.changeLanguage('javascript')">JavaScript</button>
|
|
166
|
+
<button onclick="window.changeLanguage('python')">Python</button>
|
|
167
|
+
<button onclick="window.changeLanguage('html')">HTML</button>
|
|
168
|
+
<span>Theme:</span>
|
|
169
|
+
<button onclick="window.changeTheme('tokyo-night-dark')">Tokyo Night</button>
|
|
170
|
+
<button onclick="window.changeTheme('monokai')">Monokai</button>
|
|
171
|
+
<button onclick="window.changeTheme('github-dark')">GitHub Dark</button>
|
|
172
|
+
<button onclick="window.changeTheme('atom-one-dark')">Atom One Dark</button>
|
|
173
|
+
<span>Actions:</span>
|
|
174
|
+
<button onclick="window.runCode()">▶ Run</button>
|
|
175
|
+
<button onclick="window.getCode()">Get Code</button>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
86
178
|
<div id="editor"></div>
|
|
87
|
-
|
|
179
|
+
<div id="result"></div>
|
|
180
|
+
|
|
88
181
|
<script type="module">
|
|
89
|
-
import
|
|
90
|
-
|
|
91
|
-
|
|
182
|
+
import { createTextEditor } from './components/textEditor.js';
|
|
183
|
+
|
|
184
|
+
window.language = 'javascript';
|
|
185
|
+
window.currentTheme = 'tokyo-night-dark';
|
|
186
|
+
|
|
187
|
+
const jsCode = `// Welcome to Caret!
|
|
188
|
+
function fibonacci(n) {
|
|
189
|
+
if (n <= 1) return n;
|
|
190
|
+
return fibonacci(n - 1) + fibonacci(n - 2);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Calculate first 10 Fibonacci numbers
|
|
194
|
+
for (let i = 0; i < 10; i++) {
|
|
195
|
+
console.log(\`F(\${i}) = \${fibonacci(i)}\`);
|
|
196
|
+
}`;
|
|
197
|
+
|
|
198
|
+
const pyCode = `# Welcome to Caret!
|
|
199
|
+
def fibonacci(n):
|
|
200
|
+
if n < 1:
|
|
201
|
+
return n
|
|
202
|
+
return fibonacci(n - 1) + fibonacci(n - 2)
|
|
203
|
+
|
|
204
|
+
# Calculate first 25 Fibonacci numbers
|
|
205
|
+
for i in range(25):
|
|
206
|
+
print(" i: " + f'{i} = {fibonacci(i)}. ')`;
|
|
207
|
+
|
|
208
|
+
const htmlCode = `<!-- Welcome to Caret! -->
|
|
209
|
+
<!DOCTYPE html>
|
|
210
|
+
<html lang="en">
|
|
211
|
+
<head>
|
|
212
|
+
<meta charset="UTF-8">
|
|
213
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
214
|
+
<title>Caret HTML Demo</title>
|
|
215
|
+
</head>
|
|
216
|
+
<body>
|
|
217
|
+
<h1>Hello, Caret!</h1>
|
|
218
|
+
<p>This is a simple HTML example to demonstrate Caret's capabilities.</p>
|
|
219
|
+
<ul>
|
|
220
|
+
<li>HTML5 support</li>
|
|
221
|
+
<li>Syntax highlighting</li>
|
|
222
|
+
<li>Auto-completion</li>
|
|
223
|
+
</ul>
|
|
224
|
+
</body>
|
|
225
|
+
</html>`;
|
|
226
|
+
|
|
227
|
+
const editorInstance = await createTextEditor(
|
|
92
228
|
document.getElementById('editor'),
|
|
229
|
+
jsCode,
|
|
230
|
+
'demo-editor',
|
|
93
231
|
{
|
|
94
|
-
|
|
232
|
+
dark: true,
|
|
95
233
|
language: 'javascript',
|
|
96
|
-
|
|
234
|
+
hlTheme: 'tokyo-night-dark',
|
|
235
|
+
focusColor: '#7116d8',
|
|
236
|
+
id: Math.floor(Math.random() * 1000000000),
|
|
237
|
+
theme: {
|
|
238
|
+
dark: {
|
|
239
|
+
'background.editor': '#1a1a2e',
|
|
240
|
+
'background.lineCounter': '#16213e',
|
|
241
|
+
'color.editor': '#d4d4d4',
|
|
242
|
+
'color.lineCounter': '#888',
|
|
243
|
+
'editor.caret': '#7116d8'
|
|
244
|
+
},
|
|
245
|
+
light: {
|
|
246
|
+
'background.editor': '#fff',
|
|
247
|
+
'background.lineCounter': '#f0f0f0',
|
|
248
|
+
'color.editor': '#000',
|
|
249
|
+
'color.lineCounter': '#666',
|
|
250
|
+
'editor.caret': '#7116d8'
|
|
251
|
+
}
|
|
252
|
+
}
|
|
97
253
|
}
|
|
98
254
|
);
|
|
99
|
-
</script>
|
|
100
|
-
</body>
|
|
101
|
-
</html>
|
|
102
|
-
```
|
|
103
255
|
|
|
104
|
-
|
|
256
|
+
window.editorInstance = editorInstance;
|
|
105
257
|
|
|
106
|
-
|
|
107
|
-
|
|
258
|
+
window.changeLanguage = async (lang) => {
|
|
259
|
+
window.language = lang;
|
|
260
|
+
await editorInstance.setLanguage(lang);
|
|
261
|
+
if (lang === 'javascript') editorInstance.setValue(jsCode);
|
|
262
|
+
if (lang === 'python') editorInstance.setValue(pyCode);
|
|
263
|
+
if (lang === 'html') editorInstance.setValue(htmlCode);
|
|
264
|
+
};
|
|
108
265
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
);
|
|
266
|
+
window.changeTheme = (theme) => {
|
|
267
|
+
window.currentTheme = theme;
|
|
268
|
+
// update the hlTheme stylesheet
|
|
269
|
+
const link = document.getElementById('caret-theme-demo-editor');
|
|
270
|
+
if (link) link.href = `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/${theme}.css`;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
window.runCode = async () => {
|
|
274
|
+
const resultEl = document.getElementById('result');
|
|
275
|
+
resultEl.textContent = 'Running...';
|
|
276
|
+
|
|
277
|
+
if (window.language === 'html') {
|
|
278
|
+
resultEl.innerHTML = editorInstance.getValue();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const code = editorInstance.getValue();
|
|
283
|
+
const lang = window.language;
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const res = await fetch('https://lexius-transpiler.onrender.com/run', {
|
|
287
|
+
method: 'POST',
|
|
288
|
+
headers: { 'Content-Type': 'application/json' },
|
|
289
|
+
body: JSON.stringify({ code, lang })
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (!res.ok) throw new Error(`Server error: ${res.status}`);
|
|
293
|
+
|
|
294
|
+
const data = await res.json();
|
|
295
|
+
const exec = data.result ?? {};
|
|
296
|
+
const stdout = exec.stdout ?? '';
|
|
297
|
+
const stderr = exec.stderr ?? '';
|
|
298
|
+
const result = exec.result !== undefined && exec.result !== null
|
|
299
|
+
? String(exec.result) : '';
|
|
300
|
+
|
|
301
|
+
resultEl.textContent = [stdout, stderr, result].filter(Boolean).join('\n') + '\n';
|
|
302
|
+
} catch (err) {
|
|
303
|
+
resultEl.textContent = 'Error: ' + (err.message || err) + '\n';
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
window.getCode = async () => {
|
|
308
|
+
try {
|
|
309
|
+
await navigator.clipboard.writeText(editorInstance.getValue());
|
|
310
|
+
window.confirm('Text copied to clipboard');
|
|
311
|
+
} catch (err) {
|
|
312
|
+
window.alert('Failed to copy: ', err);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
</script>
|
|
316
|
+
</body>
|
|
317
|
+
</html>
|
|
118
318
|
```
|
|
119
319
|
|
|
120
320
|
## Project Structure
|
|
121
321
|
|
|
122
322
|
```
|
|
123
323
|
caret/
|
|
124
|
-
├──
|
|
125
|
-
├── .
|
|
126
|
-
├── .
|
|
127
|
-
├──
|
|
128
|
-
├──
|
|
129
|
-
|
|
130
|
-
├──
|
|
131
|
-
├──
|
|
132
|
-
├──
|
|
133
|
-
├──
|
|
134
|
-
|
|
324
|
+
├── components/
|
|
325
|
+
│ ├── textEditor.js # Text editor(core) — EditContext, undo/redo, highlighting
|
|
326
|
+
│ ├── caret.js # Custom caret positioning via Range API
|
|
327
|
+
│ ├── lineCounter.js # Line number display
|
|
328
|
+
│ ├── font.js # Custom font loading
|
|
329
|
+
│ └── languages.js # Highlight.js language registration
|
|
330
|
+
├── .gitignore
|
|
331
|
+
├── .npmignore
|
|
332
|
+
├── index.js # Main file
|
|
333
|
+
├── LICENSE
|
|
334
|
+
├── logo.svg
|
|
335
|
+
├── package-lock.json
|
|
336
|
+
├── package.json
|
|
337
|
+
├── package.json
|
|
338
|
+
├── README.md
|
|
339
|
+
└── utilities.js # Shared utilities
|
|
135
340
|
```
|
|
136
341
|
|
|
137
342
|
## Usage
|
|
138
343
|
|
|
139
|
-
###
|
|
344
|
+
### Basic Editor
|
|
140
345
|
|
|
141
346
|
```javascript
|
|
142
|
-
import
|
|
143
|
-
const jsEditor = await editor.editor.createEditor(
|
|
144
|
-
document.getElementById('js-editor'),
|
|
145
|
-
{
|
|
146
|
-
value: `function greet(name) {
|
|
147
|
-
return \`Hello, \${name}!\`;
|
|
148
|
-
}
|
|
347
|
+
import { createEditor } from './node_modules/@pfmcodes/caret/index.js';
|
|
149
348
|
|
|
150
|
-
|
|
349
|
+
const editor = await createEditor(
|
|
350
|
+
document.getElementById('editor'), // parent element
|
|
351
|
+
'const x = 42;', // initial content
|
|
352
|
+
'my-editor', // unique id
|
|
353
|
+
{
|
|
354
|
+
dark: true,
|
|
151
355
|
language: 'javascript',
|
|
152
|
-
|
|
356
|
+
hlTheme: 'tokyo-night-dark'
|
|
153
357
|
}
|
|
154
358
|
);
|
|
155
359
|
```
|
|
156
360
|
|
|
157
|
-
###
|
|
361
|
+
### Read-Only Display
|
|
158
362
|
|
|
159
363
|
```javascript
|
|
160
|
-
|
|
161
|
-
document.getElementById('py-editor'),
|
|
162
|
-
{
|
|
163
|
-
value: `def fibonacci(n):
|
|
164
|
-
if n <= 1:
|
|
165
|
-
return n
|
|
166
|
-
return fibonacci(n-1) + fibonacci(n-2)
|
|
364
|
+
import { createEditor } from './node_modules/@pfmcodes/caret/index.js';
|
|
167
365
|
|
|
168
|
-
|
|
366
|
+
const editor = await createEditor(
|
|
367
|
+
document.getElementById('editor'),
|
|
368
|
+
code,
|
|
369
|
+
'readonly-editor',
|
|
370
|
+
{
|
|
371
|
+
dark: true,
|
|
169
372
|
language: 'python',
|
|
170
|
-
|
|
373
|
+
hlTheme: 'github-dark',
|
|
374
|
+
lock: true
|
|
171
375
|
}
|
|
172
376
|
);
|
|
173
377
|
```
|
|
174
378
|
|
|
175
|
-
###
|
|
379
|
+
### Multiple Instances
|
|
176
380
|
|
|
177
381
|
```javascript
|
|
178
|
-
|
|
179
|
-
document.getElementById('empty-editor'),
|
|
180
|
-
{
|
|
181
|
-
value: '',
|
|
182
|
-
language: 'javascript',
|
|
183
|
-
theme: 'hybrid'
|
|
184
|
-
}
|
|
185
|
-
);
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## Customization
|
|
189
|
-
|
|
190
|
-
### Custom Styling
|
|
191
|
-
|
|
192
|
-
The editor comes with default styles that you can override:
|
|
382
|
+
import { createEditor } from './node_modules/@pfmcodes/caret/index.js';
|
|
193
383
|
|
|
194
|
-
|
|
195
|
-
/* Custom editor styling */
|
|
196
|
-
#editor {
|
|
197
|
-
width: 800px !important;
|
|
198
|
-
height: 500px !important;
|
|
199
|
-
font-size: 16px !important;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/* Customize line numbers */
|
|
203
|
-
.Caret-lineCounter {
|
|
204
|
-
background: #1e1e1e;
|
|
205
|
-
}
|
|
384
|
+
// each editor needs a unique id
|
|
206
385
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
padding: 0 8px;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/* Customize the textarea */
|
|
213
|
-
#Caret-textarea {
|
|
214
|
-
font-family: 'Fira Code', 'Consolas', monospace;
|
|
215
|
-
line-height: 1.6;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/* Customize the Caret */
|
|
219
|
-
#Caret-caret {
|
|
220
|
-
background: #00ff00 !important;
|
|
221
|
-
width: 3px !important;
|
|
222
|
-
}
|
|
386
|
+
const editor1 = await createEditor(el1, code1, 'editor-1', options);
|
|
387
|
+
const editor2 = await createEditor(el2, code2, 'editor-2', options);
|
|
223
388
|
```
|
|
224
389
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
### Multi-Language Support
|
|
228
|
-
|
|
229
|
-
Caret supports all languages available in Highlight.js:
|
|
390
|
+
### Custom Theme
|
|
230
391
|
|
|
231
392
|
```javascript
|
|
232
|
-
|
|
233
|
-
await editor.editor.createEditor(el, { language: 'javascript', ... });
|
|
234
|
-
|
|
235
|
-
// Python
|
|
236
|
-
await editor.editor.createEditor(el, { language: 'python', ... });
|
|
237
|
-
|
|
238
|
-
// TypeScript
|
|
239
|
-
await editor.editor.createEditor(el, { language: 'typescript', ... });
|
|
240
|
-
|
|
241
|
-
// HTML
|
|
242
|
-
await editor.editor.createEditor(el, { language: 'html', ... });
|
|
243
|
-
|
|
244
|
-
// CSS
|
|
245
|
-
await editor.editor.createEditor(el, { language: 'css', ... });
|
|
393
|
+
import { createEditor } from './node_modules/@pfmcodes/caret/index.js';
|
|
246
394
|
|
|
247
|
-
|
|
395
|
+
const editor = await createEditor(
|
|
396
|
+
document.getElementById('editor'),
|
|
397
|
+
code,
|
|
398
|
+
'editor-1',
|
|
399
|
+
{
|
|
400
|
+
dark: true,
|
|
401
|
+
language: 'javascript',
|
|
402
|
+
hlTheme: 'tokyo-night-dark',
|
|
403
|
+
focusColor: '#fff',
|
|
404
|
+
theme: {
|
|
405
|
+
dark: {
|
|
406
|
+
'background.editor': '#222',
|
|
407
|
+
'background.lineCounter': '#333',
|
|
408
|
+
'color.editor': '#d4d4d4',
|
|
409
|
+
'color.lineCounter': '#d4d4d4',
|
|
410
|
+
'editor.caret': '#37ff29'
|
|
411
|
+
},
|
|
412
|
+
light: {
|
|
413
|
+
'background.editor': '#cfd8f7',
|
|
414
|
+
'background.lineCounter': '#e2e7f9',
|
|
415
|
+
'color.editor': '#000',
|
|
416
|
+
'color.lineCounter': '#000',
|
|
417
|
+
'editor.caret': '#37ff29'
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
);
|
|
248
422
|
```
|
|
249
423
|
|
|
250
|
-
###
|
|
424
|
+
### Custom Font
|
|
251
425
|
|
|
252
426
|
```javascript
|
|
253
|
-
|
|
254
|
-
value: 'console.log("Hello");',
|
|
255
|
-
language: 'javascript',
|
|
256
|
-
theme: 'hybrid'
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// Later, switch to Python
|
|
260
|
-
editorInstance.setValue('print("Hello")');
|
|
261
|
-
editorInstance.setLanguage('python');
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### Real-Time Code Editing
|
|
265
|
-
|
|
266
|
-
The editor automatically:
|
|
267
|
-
- Updates syntax highlighting as you type
|
|
268
|
-
- Adjusts line numbers dynamically
|
|
269
|
-
- Maintains Caret position accurately
|
|
270
|
-
- Synchronizes all visual components
|
|
427
|
+
import { createEditor } from './node_modules/@pfmcodes/caret/index.js';
|
|
271
428
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
containerElement,
|
|
429
|
+
const editor = await createEditor(
|
|
430
|
+
document.getElementById('editor'),
|
|
431
|
+
code,
|
|
432
|
+
'editor-1',
|
|
277
433
|
{
|
|
278
|
-
|
|
279
|
-
value: 'const x = 42;',
|
|
280
|
-
|
|
281
|
-
// Programming language for syntax highlighting
|
|
282
|
-
// Supports: javascript, python, java, cpp, html, css, json, etc.
|
|
434
|
+
dark: true,
|
|
283
435
|
language: 'javascript',
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
436
|
+
font: {
|
|
437
|
+
url: './fonts/FiraCode.ttf',
|
|
438
|
+
name: 'Fira Code'
|
|
439
|
+
}
|
|
288
440
|
}
|
|
289
441
|
);
|
|
290
442
|
```
|
|
291
443
|
|
|
292
|
-
### Available Themes
|
|
293
|
-
|
|
294
|
-
Caret supports all Highlight.js themes. Popular options include:
|
|
295
|
-
|
|
296
|
-
**Dark Themes:**
|
|
297
|
-
- `atom-one-dark`
|
|
298
|
-
- `monokai`
|
|
299
|
-
- `night-owl`
|
|
300
|
-
- `nord`
|
|
301
|
-
- `tokyo-night-dark`
|
|
302
|
-
- `vs2015`
|
|
303
|
-
|
|
304
|
-
**Light Themes:**
|
|
305
|
-
- `github`
|
|
306
|
-
- `atom-one-light`
|
|
307
|
-
- `stackoverflow-light`
|
|
308
|
-
- `xcode`
|
|
309
|
-
|
|
310
444
|
## API Reference
|
|
311
445
|
|
|
312
|
-
### createEditor(
|
|
446
|
+
### createEditor(parent, content, id, options)
|
|
313
447
|
|
|
314
448
|
Creates a new editor instance.
|
|
315
449
|
|
|
316
450
|
**Parameters:**
|
|
317
|
-
- `container` (HTMLElement) - The DOM element to attach the editor to
|
|
318
|
-
- `options` (Object) - Configuration options
|
|
319
|
-
- `value` (string) - Initial code content
|
|
320
|
-
- `language` (string) - Programming language for syntax highlighting
|
|
321
|
-
- `theme` (string) - Highlight.js theme name
|
|
322
451
|
|
|
323
|
-
|
|
452
|
+
| Parameter | Type | Required | Description |
|
|
453
|
+
|-----------|------|----------|-------------|
|
|
454
|
+
| `parent` | `HTMLElement` | ✅ | Container element |
|
|
455
|
+
| `content` | `string` | ✅ | Initial content |
|
|
456
|
+
| `options` | `object` | ✅ | Configuration |
|
|
457
|
+
|
|
458
|
+
**Options:**
|
|
459
|
+
|
|
460
|
+
| Option | Type | Default | Description |
|
|
461
|
+
|--------|------|---------|-------------|
|
|
462
|
+
| `dark` | `boolean` | `false` | Dark mode |
|
|
463
|
+
| `shadow` | `boolean` | `true` | Box shadow |
|
|
464
|
+
| `focusColor` | `string` | `#7c3aed` | Border color on focus |
|
|
465
|
+
| `shadowColor` | `string` | `#000` | Shadow color |
|
|
466
|
+
| `lock` | `boolean` | `false` | Read-only mode |
|
|
467
|
+
| `language` | `string` | `plaintext` | Highlight.js language |
|
|
468
|
+
| `hlTheme` | `string` | `hybrid` | Highlight.js theme |
|
|
469
|
+
| `font` | `object` | — | Custom font `{ url, name }` |
|
|
470
|
+
| `theme` | `object` | — | Custom colors (see above) |
|
|
471
|
+
`id` | `string/number` | — | required,
|
|
472
|
+
|
|
473
|
+
**Returns:** `Promise<EditorInstance>`
|
|
324
474
|
|
|
325
475
|
### EditorInstance Methods
|
|
326
476
|
|
|
327
477
|
```javascript
|
|
328
|
-
// Get current
|
|
329
|
-
const code =
|
|
478
|
+
// Get current content
|
|
479
|
+
const code = editor.getValue();
|
|
480
|
+
|
|
481
|
+
// Set content
|
|
482
|
+
editor.setValue('console.log("hello");');
|
|
330
483
|
|
|
331
|
-
//
|
|
332
|
-
|
|
484
|
+
// Listen for changes
|
|
485
|
+
editor.onChange((text) => {
|
|
486
|
+
console.log('content changed:', text);
|
|
487
|
+
});
|
|
333
488
|
|
|
334
|
-
//
|
|
335
|
-
|
|
489
|
+
// Check if focused
|
|
490
|
+
const focused = editor.isFocused();
|
|
336
491
|
|
|
337
|
-
//
|
|
338
|
-
|
|
492
|
+
// Switch language
|
|
493
|
+
await editor.setLanguage('python');
|
|
339
494
|
|
|
340
|
-
// Destroy
|
|
341
|
-
|
|
495
|
+
// Destroy instance and clean up
|
|
496
|
+
editor.delete();
|
|
342
497
|
```
|
|
343
498
|
|
|
344
|
-
###
|
|
499
|
+
### Keyboard Shortcuts
|
|
345
500
|
|
|
346
|
-
|
|
347
|
-
|
|
501
|
+
| Shortcut | Action |
|
|
502
|
+
|----------|--------|
|
|
503
|
+
| `Tab` | Indent (4 spaces) |
|
|
504
|
+
| `Shift+Tab` | Unindent |
|
|
505
|
+
| `Ctrl+Z` | Undo |
|
|
506
|
+
| `Ctrl+Y` | Redo |
|
|
507
|
+
| `Ctrl+Shift+Z` | Redo |
|
|
348
508
|
|
|
349
|
-
|
|
350
|
-
- **Tab** - Indent selected lines or insert 4 spaces
|
|
351
|
-
- **Shift + Tab** - Unindent selected lines
|
|
509
|
+
### Global Undo/Redo Stack
|
|
352
510
|
|
|
353
|
-
|
|
354
|
-
The editor features a custom-rendered Caret that adapts to your theme (light/dark).
|
|
511
|
+
Each editor instance stores its undo/redo stack on `window.caret`:
|
|
355
512
|
|
|
356
|
-
|
|
357
|
-
|
|
513
|
+
```javascript
|
|
514
|
+
// access undo stack for a specific editor
|
|
515
|
+
window.caret['undoStack.editor-1']; // array of { content, cursor }
|
|
516
|
+
window.caret['redoStack.editor-1'];
|
|
517
|
+
```
|
|
358
518
|
|
|
359
|
-
##
|
|
519
|
+
## Customization
|
|
360
520
|
|
|
361
|
-
|
|
521
|
+
### CSS Override
|
|
362
522
|
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
<title>Caret Demo</title>
|
|
370
|
-
<link rel="stylesheet" href="./index.css">
|
|
371
|
-
<style>
|
|
372
|
-
body {
|
|
373
|
-
font-family: system-ui, -apple-system, sans-serif;
|
|
374
|
-
padding: 20px;
|
|
375
|
-
background: #f5f5f5;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
#editor {
|
|
379
|
-
width: 900px;
|
|
380
|
-
height: 600px;
|
|
381
|
-
margin: 20px auto;
|
|
382
|
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
383
|
-
border-radius: 8px;
|
|
384
|
-
overflow: hidden;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
.controls {
|
|
388
|
-
max-width: 900px;
|
|
389
|
-
margin: 0 auto 20px;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
button {
|
|
393
|
-
padding: 8px 16px;
|
|
394
|
-
margin-right: 8px;
|
|
395
|
-
border: none;
|
|
396
|
-
background: #7116d8;
|
|
397
|
-
color: white;
|
|
398
|
-
border-radius: 4px;
|
|
399
|
-
cursor: pointer;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
button:hover {
|
|
403
|
-
background: #5a11ab;
|
|
404
|
-
}
|
|
405
|
-
</style>
|
|
406
|
-
</head>
|
|
407
|
-
<body>
|
|
408
|
-
<h1 style="text-align: center;">Caret Demo</h1>
|
|
409
|
-
|
|
410
|
-
<div class="controls">
|
|
411
|
-
<button onclick="window.changeLanguage('javascript')">JavaScript</button>
|
|
412
|
-
<button onclick="window.changeLanguage('python')">Python</button>
|
|
413
|
-
<button onclick="window.changeLanguage('html')">HTML</button>
|
|
414
|
-
<button onclick="window.changeTheme('monokai')">Monokai</button>
|
|
415
|
-
<button onclick="window.changeTheme('github-dark')">GitHub</button>
|
|
416
|
-
<button onclick="window.changeTheme('atom-one-dark')">Atom One Dark</button>
|
|
417
|
-
<button onclick="window.getCode()">Get Code</button>
|
|
418
|
-
</div>
|
|
419
|
-
|
|
420
|
-
<div id="editor"></div>
|
|
421
|
-
|
|
422
|
-
<script type="module">
|
|
423
|
-
import editor from './node_modules/@pfmcodes/caret/index.js';
|
|
424
|
-
|
|
425
|
-
// Initialize editor
|
|
426
|
-
const editorInstance = await editor.editor.createEditor(
|
|
427
|
-
document.getElementById('editor'),
|
|
428
|
-
{
|
|
429
|
-
value: `// Welcome to Caret!
|
|
430
|
-
function fibonacci(n) {
|
|
431
|
-
if (n <= 1) return n;
|
|
432
|
-
return fibonacci(n - 1) + fibonacci(n - 2);
|
|
523
|
+
```css
|
|
524
|
+
/* editor container */
|
|
525
|
+
#editor {
|
|
526
|
+
width: 900px;
|
|
527
|
+
height: 500px;
|
|
528
|
+
font-size: 16px;
|
|
433
529
|
}
|
|
434
530
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
// Make it globally accessible for demo buttons
|
|
445
|
-
window.editorInstance = editorInstance;
|
|
446
|
-
|
|
447
|
-
window.changeLanguage = (lang) => {
|
|
448
|
-
editorInstance.setLanguage(lang);
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
window.changeTheme = (theme) => {
|
|
452
|
-
editor.theme.setTheme(theme);
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
window.getCode = () => {
|
|
456
|
-
const code = editorInstance.getValue();
|
|
457
|
-
console.log(code);
|
|
458
|
-
alert('Code copied to console!');
|
|
459
|
-
};
|
|
460
|
-
</script>
|
|
461
|
-
</body>
|
|
462
|
-
</html>
|
|
531
|
+
/* line numbers */
|
|
532
|
+
.lineCounter {
|
|
533
|
+
min-width: 40px;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.line-number {
|
|
537
|
+
padding: 0 8px;
|
|
538
|
+
}
|
|
463
539
|
```
|
|
464
540
|
|
|
465
|
-
|
|
541
|
+
### Available hlTheme values
|
|
466
542
|
|
|
467
|
-
|
|
543
|
+
**Dark:**
|
|
544
|
+
- `atom-one-dark`
|
|
545
|
+
- `monokai`
|
|
546
|
+
- `night-owl`
|
|
547
|
+
- `nord`
|
|
548
|
+
- `tokyo-night-dark`
|
|
549
|
+
- `vs2015`
|
|
550
|
+
- `hybrid`
|
|
551
|
+
- `github-dark`
|
|
552
|
+
|
|
553
|
+
**Light:**
|
|
554
|
+
- `github`
|
|
555
|
+
- `atom-one-light`
|
|
556
|
+
- `stackoverflow-light`
|
|
557
|
+
- `xcode`
|
|
468
558
|
|
|
469
|
-
|
|
559
|
+
## Browser Support
|
|
470
560
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
561
|
+
| Browser | Support |
|
|
562
|
+
|---------|---------|
|
|
563
|
+
| Chrome / Chromium | Full support |
|
|
564
|
+
| Edge | Full support |
|
|
565
|
+
| Firefox | EditContext not yet supported |
|
|
566
|
+
| Safari | EditContext not yet supported |
|
|
475
567
|
|
|
476
|
-
|
|
477
|
-
- Typing (input events)
|
|
478
|
-
- Scrolling (scroll events)
|
|
479
|
-
- Navigation (click, keyup events)
|
|
568
|
+
Caret v0.3.0 uses the [EditContext API](https://developer.mozilla.org/en-US/docs/Web/API/EditContext_API) which is currently only available in Chromium-based browsers. Firefox support is tracked [here](https://bugzilla.mozilla.org/show_bug.cgi?id=1850301).
|
|
480
569
|
|
|
481
|
-
|
|
570
|
+
## Performance
|
|
482
571
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
572
|
+
| Metric | Caret v0.3.0 | Monaco | CodeMirror 6 |
|
|
573
|
+
|--------|-------------|--------|--------------|
|
|
574
|
+
| Bundle size | **26KB** | ~5MB | ~400KB |
|
|
575
|
+
| Load time | **~42ms** | ~2-3s | ~500ms |
|
|
576
|
+
| Lines of code | **551** | ~300,000 | ~50,000 |
|
|
577
|
+
| Architecture | EditContext | textarea | contenteditable |
|
|
487
578
|
|
|
488
|
-
|
|
579
|
+
## How It Works
|
|
489
580
|
|
|
490
|
-
|
|
491
|
-
- Chrome, Firefox, Safari, Edge (latest versions)
|
|
492
|
-
- Requires JavaScript modules support
|
|
581
|
+
Caret v0.3.0 uses Chrome's EditContext API to completely separate input handling from rendering:
|
|
493
582
|
|
|
494
|
-
|
|
583
|
+
1. **EditContext** receives all keyboard input, IME, clipboard events
|
|
584
|
+
2. **Text model** — a single string `text` is the source of truth
|
|
585
|
+
3. **render()** — calls `hljs.highlight()` and sets `main.innerHTML`
|
|
586
|
+
4. **Caret** — positioned via `Range.getBoundingClientRect()`, no canvas math
|
|
587
|
+
5. **Undo/redo** — pure string operations stored in `window.caret`
|
|
495
588
|
|
|
496
|
-
|
|
589
|
+
No dual-layer sync issues. No textarea fighting. No canvas measurements.
|
|
590
|
+
|
|
591
|
+
## Contributing
|
|
497
592
|
|
|
498
593
|
1. Fork the repository
|
|
499
594
|
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
500
|
-
3. Commit your changes (`git commit -m 'Add
|
|
595
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
501
596
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
502
597
|
5. Open a Pull Request
|
|
503
598
|
|
|
504
|
-
### Development Setup
|
|
505
|
-
|
|
506
|
-
```bash
|
|
507
|
-
# Clone the repository
|
|
508
|
-
git clone https://github.com/pfmcodes/lexius-editor.git
|
|
509
|
-
|
|
510
|
-
# Navigate to the directory
|
|
511
|
-
cd lexius-editor
|
|
512
|
-
|
|
513
|
-
# Install dependencies
|
|
514
|
-
npm install
|
|
515
|
-
|
|
516
|
-
# Run development server (if applicable)
|
|
517
|
-
npm run dev
|
|
518
|
-
|
|
519
|
-
# Build the project
|
|
520
|
-
npm run build
|
|
521
|
-
```
|
|
522
|
-
|
|
523
599
|
## License
|
|
524
600
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
## Performance Notes
|
|
528
|
-
- Handles 10k+ lines smoothly in all browsers
|
|
529
|
-
- Firefox DevTools can inspect up to 3M lines without breaking a sweat
|
|
530
|
-
- Chrome DevTools politely requests you don't inspect past 300k lines
|
|
531
|
-
|
|
532
|
-
## Acknowledgments
|
|
533
|
-
|
|
534
|
-
- Built with modern JavaScript/TypeScript
|
|
535
|
-
- Syntax highlighting powered by Highlight.js
|
|
536
|
-
- Inspired by various text editor projects in the JavaScript ecosystem
|
|
537
|
-
|
|
538
|
-
## Support
|
|
539
|
-
|
|
540
|
-
- **Issues**: [GitHub Issues](https://github.com/pfmcodes/lexius-editor/issues)
|
|
541
|
-
- **Discussions**: [GitHub Discussions](https://github.com/pfmcodes/lexius-editor/discussions)
|
|
542
|
-
|
|
543
|
-
## Links
|
|
544
|
-
|
|
545
|
-
- [GitHub Repository](https://github.com/pfmcodes/lexius-editor)
|
|
546
|
-
- [NPM Package](https://www.npmjs.com/package/@pfmcodes/caret) *(if published)*
|
|
601
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
|
547
602
|
|
|
548
603
|
---
|
|
549
604
|
|
|
550
|
-
Made with ❤️ by [PFMCODES](https://github.com/PFMCODES)
|
|
605
|
+
Made with ❤️ by [PFMCODES](https://github.com/PFMCODES)
|