npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pfmcodes/caret

v0.3.2

Published

The official code editor engine for lexius

Readme

LOGO

Caret

License: MIT Downloads Version

A lightweight, fast code editor engine with real-time syntax highlighting, custom caret rendering, and a clean EditContext-based architecture. 605 lines. ~30KB. ~27ms load time.

Features

  • EditContext API - Clean input handling with no browser fighting, no textarea hacks
  • Live Syntax Highlighting - Real-time highlighting powered by Highlight.js
  • Custom Caret - Pixel-perfect caret positioning via Range API
  • Line Numbers - Built-in line counter with dynamic updates
  • Smart Indentation - Tab/Shift+Tab for indenting and unindenting code blocks
  • Undo/Redo - Full undo/redo stack with cursor position restoration (Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z)
  • Arrow Key Movement - Full arrow key navigation with column preservation
  • Theme Support - Custom background, text, caret and line counter colors
  • Lock Mode - Read-only mode for displaying code
  • Font Support - Custom font loading
  • Paste Handling - Always pastes plain text, no rich HTML
  • Browser Detection - Friendly error message for unsupported browsers
  • ES Modules - Modern ESM architecture
  • Lightweight - 605 lines, ~30KB total, loads in ~27ms

Table of Contents

What's New

v0.3.1 — API update, bug fixes and README changes

  • Arrow key movement (←→↑↓) - previously did not work, now fully functional with column preservation
  • getCursor() - get the cursor/caret's current position
  • setCursor(pos) - set cursor/caret position to a specific character offset
  • onCursorMove(cb) - triggers a callback when the cursor moves
  • undo() and redo() exposed in API - call them programmatically
  • Enter key fix - newline rendering fixed with zero-width space anchor
  • Browser detection - friendly error message for non-Chromium browsers with link to file an issue
  • Updated JSDoc documentation

v0.3.0 — Complete Rewrite

  • Ditched textarea — rebuilt on Chrome's EditContext API
  • No more sync issues — single text model, no dual layer fighting
  • Undo/redo with cursor restoration — cursor returns to exact position
  • Pixel-perfect caret — positioned via Range API, no canvas math
  • Modular architecturetextEditor.js, caret.js, lineCounter.js, font.js, languages.js
  • setLanguage() — switch language at runtime
  • delete() — clean teardown with full event listener removal
  • onClick cursor positioning — click anywhere to place cursor

v0.2.8

  • Bug fixes

v0.2.7

  • File optimizations and updated comments

v0.2.6

  • CommonJS support removed to reduce package size

v0.2.5

  • Multiple editor instances support
  • Word-aware line wrapping
  • Fixed language re-registration bug

Installation

NPM

npm install @pfmcodes/caret

Yarn

yarn add @pfmcodes/caret

PNPM

pnpm add @pfmcodes/caret

Quick Start

Quick Start Guide Video

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Caret Demo</title>
  <link rel="shortcut icon" href="logo.svg" type="image/svg">
  <style>
    body {
      font-family: system-ui, -apple-system, sans-serif;
      padding: 20px;
      background: #1a1a2e;
      color: #fff;
    }

    h1 {
      text-align: center;
      margin-bottom: 20px;
    }

    #editor {
      width: 900px;
      height: 600px;
      margin: 20px auto;
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
      border-radius: 8px;
      overflow: hidden;
    }

    #result {
      width: 900px;
      margin: 10px auto;
      padding: 10px;
      background: #111;
      color: #0f0;
      font-family: monospace;
      font-size: 13px;
      border-radius: 8px;
      min-height: 40px;
      white-space: pre-wrap;
    }

    .controls {
      max-width: 900px;
      margin: 0 auto 20px;
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
    }

    .controls span {
      font-size: 12px;
      opacity: 0.5;
      align-self: center;
      margin: 0 4px;
    }

    button {
      padding: 8px 16px;
      border: none;
      background: #7116d8;
      color: white;
      border-radius: 4px;
      cursor: pointer;
      font-size: 13px;
    }

    button:hover {
      background: #5a11ab;
    }
  </style>
</head>
<body>
  <h1>Caret Demo</h1>

  <div class="controls">
    <span>Language:</span>
    <button onclick="window.changeLanguage('javascript')">JavaScript</button>
    <button onclick="window.changeLanguage('python')">Python</button>
    <button onclick="window.changeLanguage('html')">HTML</button>
    <span>Theme:</span>
    <button onclick="window.changeTheme('tokyo-night-dark')">Tokyo Night</button>
    <button onclick="window.changeTheme('monokai')">Monokai</button>
    <button onclick="window.changeTheme('github-dark')">GitHub Dark</button>
    <button onclick="window.changeTheme('atom-one-dark')">Atom One Dark</button>
    <span>Actions:</span>
    <button onclick="window.runCode()">▶ Run</button>
    <button onclick="window.getCode()">Get Code</button>
  </div>

  <div id="editor"></div>
  <div id="result"></div>

  <script type="module">
    import Caret from './index.js';

    window.language = 'javascript';
    window.currentTheme = 'tokyo-night-dark';

    const jsCode = `// Welcome to Caret!
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// Calculate first 10 Fibonacci numbers
for (let i = 0; i < 10; i++) {
  console.log(\`F(\${i}) = \${fibonacci(i)}\`);
}`;

    const pyCode = `# Welcome to Caret!
def fibonacci(n):
    if n < 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# Calculate first 25 Fibonacci numbers
for i in range(25):
    print(" i: " + f'{i} = {fibonacci(i)}. ')`;

    const htmlCode = `<!-- Welcome to Caret! -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Caret HTML Demo</title>
</head>
<body>
  <h1>Hello, Caret!</h1>
  <p>This is a simple HTML example to demonstrate Caret's capabilities.</p>
  <ul>
    <li>HTML5 support</li>
    <li>Syntax highlighting</li>
    <li>Auto-completion</li>
  </ul>
</body>
</html>`;

    const editorInstance = await Caret.createEditor(
      document.getElementById('editor'),
      jsCode,
      {
        dark: true,
        language: 'javascript',
        id: `${Math.floor(Math.random() * 1000000000)}`,
        hlTheme: 'tokyo-night-dark',
        focusColor: '#7116d8',
        theme: {
          dark: {
            'background.editor': '#1a1a2e',
            'background.lineCounter': '#16213e',
            'color.editor': '#d4d4d4',
            'color.lineCounter': '#888',
            'editor.caret': '#7116d8'
          },
          light: {
            'background.editor': '#fff',
            'background.lineCounter': '#f0f0f0',
            'color.editor': '#000',
            'color.lineCounter': '#666',
            'editor.caret': '#7116d8'
          }
        }
      }
    );

    window.editorInstance = editorInstance;

    window.changeLanguage = async (lang) => {
      window.language = lang;
      await editorInstance.setLanguage(lang);
      if (lang === 'javascript') editorInstance.setValue(jsCode);
      if (lang === 'python') editorInstance.setValue(pyCode);
      if (lang === 'html') editorInstance.setValue(htmlCode);
    };

    window.changeTheme = (theme) => {
      window.currentTheme = theme;
      // update the hlTheme stylesheet
      const link = document.getElementById('caret-theme-demo-editor');
      if (link) link.href = `https://esm.sh/@pfmcodes/[email protected]/styles/${theme}.css`;
    };

    window.runCode = async () => {
      const resultEl = document.getElementById('result');
      resultEl.textContent = 'Running...';

      if (window.language === 'html') {
        resultEl.innerHTML = editorInstance.getValue();
        return;
      }

      const code = editorInstance.getValue();
      const lang = window.language;

      try {
        const res = await fetch('https://lexius-transpiler.onrender.com/run', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ code, lang })
        });

        if (!res.ok) throw new Error(`Server error: ${res.status}`);

        const data = await res.json();
        const exec = data.result ?? {};
        const stdout = exec.stdout ?? '';
        const stderr = exec.stderr ?? '';
        const result = exec.result !== undefined && exec.result !== null
          ? String(exec.result) : '';

        resultEl.textContent = [stdout, stderr, result].filter(Boolean).join('\n') + '\n';
      } catch (err) {
        resultEl.textContent = 'Error: ' + (err.message || err) + '\n';
      }
    };

    window.getCode = async () => {
      try {
        await navigator.clipboard.writeText(editorInstance.getValue());
        window.confirm('Text copied to clipboard');
      } catch (err) {
        window.alert('Failed to copy: ', err);
      }
    };
  </script>
</body>
</html>

Project Structure

caret/
├── components/
│   ├── textEditor.js     # Text editor (core) — EditContext, undo/redo, highlighting
│   ├── caret.js          # Custom caret positioning via Range API
│   ├── lineCounter.js    # Line number display
│   ├── font.js           # Custom font loading
│   └── languages.js      # Highlight.js language registration
├── .gitignore
├── .npmignore
├── index.js              # Main entry point
├── LICENSE
├── package.json
├── README.md
└── utilities.js          # Shared utilities

Usage

Basic Editor

import Caret from './node_modules/@pfmcodes/caret/index.js';

const editor = await Caret.createEditor(
  document.getElementById('editor'),  // parent element
  'const x = 42;',                    // initial content
  {
    id: 'my-editor',                        // unique id
    dark: true,
    language: 'javascript',
    hlTheme: 'tokyo-night-dark'
  }
);

Read-Only Display

import Caret from './node_modules/@pfmcodes/caret/index.js';

const editor = await Caret.createEditor(
  document.getElementById('editor'),
  code,
  {
    dark: true,
    language: 'python',
    id: 'readonly-editor',
    hlTheme: 'github-dark',
    lock: true
  }
);

Multiple Instances

import Caret from './node_modules/@pfmcodes/caret/index.js';

// each editor needs a unique id
const editor1 = await Caret.createEditor(el1, code1, {id: 'editor-1', ...options});
const editor2 = await Caret.createEditor(el2, code2, { id: 'editor-2', ...options });

Custom Theme

import Caret from './node_modules/@pfmcodes/caret/index.js';

const editor = await Caret.createEditor(
  document.getElementById('editor'),
  code,
  'editor-1',
  {
    dark: true,
    language: 'javascript',
    hlTheme: 'tokyo-night-dark',
    focusColor: '#fff',
    theme: {
      dark: {
        'background.editor': '#222',
        'background.lineCounter': '#333',
        'color.editor': '#d4d4d4',
        'color.lineCounter': '#d4d4d4',
        'editor.caret': '#37ff29'
      },
      light: {
        'background.editor': '#cfd8f7',
        'background.lineCounter': '#e2e7f9',
        'color.editor': '#000',
        'color.lineCounter': '#000',
        'editor.caret': '#37ff29'
      }
    }
  }
);

Custom Font

import Caret from './node_modules/@pfmcodes/caret/index.js';

const editor = await Caret.createEditor(
  document.getElementById('editor'),
  code,
  'editor-1',
  {
    dark: true,
    language: 'javascript',
    font: {
      url: './fonts/FiraCode.ttf',
      name: 'Fira Code'
    }
  }
);

API Reference

createEditor(parent, content, id, options)

Creates a new editor instance.

Parameters:

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | parent | HTMLElement | ✓ | Container element | | content | string | ✓ | Initial content | | options | object | ✓ | Configuration |

Options:

| Option | Type | Default | Required | Description | |--------|------|---------|----------|-------------| | dark | boolean | false | ✕ | Dark mode | | id | string | - | ✓ | required to distinguish between multiple instances | | shadow | boolean | true | ✕ | Box shadow | | focusColor | string | #7c3aed | ✕ | Border color on focus | | shadowColor | string | #000 | ✕ | Shadow color | | lock | boolean | false | ✕ | Read-only mode | | language | string | plaintext | ✓ | Highlight.js language | | hlTheme | string | hybrid | ✕ | Highlight.js theme | | font | object | — | ✕ | Custom font { url, name } | | theme | object | — | ✕ | Custom colors (see more.) |

Returns: Promise<EditorInstance>

EditorInstance Methods

// Get current content (strips internal zero-width spaces)
const code = editor.getValue();

// Set content
editor.setValue('console.log("hello");');

// Get current cursor position (character offset)
const pos = editor.getCursor();

// Set cursor to a specific position
editor.setCursor(42);

// Programmatically undo
editor.undo();

// Programmatically redo
editor.redo();

// Listen for content changes
editor.onChange((text) => {
  console.log('content changed:', text);
});

// Listen for cursor movement
editor.onCursorMove((pos) => {
  console.log('cursor at:', pos);
});

// Check if focused
const focused = editor.isFocused();

// Switch language
await editor.setLanguage('python');

// Destroy instance and clean up all DOM elements and event listeners
editor.delete();

Default Colors

from option: | Property | Default | |----------|---------| | focusColor| #7c3aed | | shadowColor | #000000 |

from theme: | Property | Dark | Light | |----------|------|-------| | background.editor | #101010 | #d4d4d4 | | editor.caret | #ffffff | #111111 | | color.editor | #ffffff | #000000 | | color.lineCounter | #ffffff | #111111 | | background.lineCounter | #1e1e1e | #ffffff |

Keyboard Shortcuts

| Shortcut | Action | |----------|--------| | Tab | Indent (4 spaces) | | Shift+Tab | Unindent | | Ctrl+Z | Undo | | Ctrl+Y | Redo | | Ctrl+Shift+Z | Redo | | | Move cursor left | | | Move cursor right | | | Move cursor up (preserves column) | | | Move cursor down (preserves column) |

Global Undo/Redo Stack

Each editor instance stores its undo/redo stack on window.caret:

// access undo stack for a specific editor
window.caret['undoStack.editor-1']; // array of { content, cursor }
window.caret['redoStack.editor-1'];

Customization

CSS Override

/* editor container */
#editor {
  width: 900px;
  height: 500px;
  font-size: 16px;
}

/* line numbers */
.lineCounter {
  min-width: 40px;
}

.line-number {
  padding: 0 8px;
}

Available hlTheme values

Dark:

  • atom-one-dark
  • monokai
  • night-owl
  • nord
  • tokyo-night-dark
  • vs2015
  • hybrid
  • github-dark

Light:

  • github
  • atom-one-light
  • stackoverflow-light
  • xcode

Browser Support

| Browser | Support | |---------|---------| | Chrome / Chromium | ✓ | | Edge | ✓ | | Firefox | ✗ | | Safari | ✗ |

Caret v0.3+ uses the EditContext API which is currently only available in Chromium-based browsers. Firefox support is tracked here.

Performance

| Metric | Caret v0.3+ | Monaco | CodeMirror 6 | |--------|-------------|--------|--------------| | Bundle size | ~32.34KB | ~5MB | ~400KB | | Load time | ~25ms | ~2-3s | ~500ms | | Lines of code | 650 | ~300,000 | ~50,000 | | Architecture | EditContext | textarea | contenteditable |

Speed and Size

| Files | Blank Lines | Comments | Code | Total | |-------|-------------|----------|------|-------| | 7 | 82 | 127 | 604 | 813 |

| File Name | Language | Size | Path | Blank Lines | Comments | Code | |-----------|----------|------|------|-------------|----------|------| | index.js | JavaScript | 174 bytes | / | 1 | 0 | 6 | | utilities.js | JavaScript | 765 bytes | / | 6 | 2 | 24 | | caret.js | JavaScript | 3.04 KB | components/ | 16 | 15 | 64 | | font.js | JavaScript | 361 bytes | components/ | 2 | 8 | 6 | | languages.js | JavaScript | 3.98 KB | components/ | 7 | 7 | 88 | | lineCounter.js | JavaScript | 3.72 KB | components/ | 5 | 24 | 62 | | textEditor.js | JavaScript | 20.3 KB | components/ | 46 | 57 | 400 | | Total | — | ~32.34KB | — | 83 | 113 | 650 |

Load time on Chrome

~25ms load

Load time on Edge

~27ms load

How It Works

Caret v0.3+ uses Chrome's EditContext API to completely separate input handling from rendering:

  1. EditContext receives all keyboard input, IME, clipboard events
  2. Text model — a single string text is the source of truth
  3. render() — calls hljs.highlight() and sets main.innerHTML
  4. Caret — positioned via Range.getBoundingClientRect(), no canvas math
  5. Undo/redo — pure string operations stored in window.caret

No dual-layer sync issues. No textarea fighting. No canvas measurements.

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

MIT License — see LICENSE for details.


Made with ❤️ by PFMCODES