@connectedxm/entity-editor
v0.0.15
Published
A **zero-dependency**, lightweight React text editor with intelligent entity recognition and real-time formatting using an innovative bitmap-based architecture.
Readme
Entity Editor
A zero-dependency, lightweight React text editor with intelligent entity recognition and real-time formatting using an innovative bitmap-based architecture.
✨ Features
- 🚀 Zero Dependencies: Pure React implementation with no external libraries
- 🎯 Smart Entity Recognition: Automatically detects and styles:
- @mentions (
@username) - #interests (
#hashtag) - 🔗 Links (
https://example.com)
- @mentions (
- 📝 Rich Text Formatting: Bold, italic, underline, strikethrough with keyboard shortcuts
- ⚡ Real-time Processing: Instant styling as you type with optimized performance
- 🗜️ Bitmap Architecture: Ultra-efficient character-level state tracking using bit operations
- 🎨 Customizable Styling: Configurable colors and styles for all entity types
- 🔍 Search & Autocomplete: Built-in search functionality for mentions and interests
- ⌨️ Keyboard Shortcuts: Cmd/Ctrl+B/I/U/Shift+S for formatting
- 📱 Accessible: Built on standard
contentEditablewith proper cursor management
🏗️ Bitmap Architecture
The editor's core innovation is its bitmap-based character tracking system. Instead of storing complex objects for each formatting state, every character position in the text is represented by a single number where each bit represents a different property:
// Each character's state encoded in 8 bits (1 byte)
const BOLD_MASK = 0b00000010; // Bit 1: Bold formatting
const ITALIC_MASK = 0b00000100; // Bit 2: Italic formatting
const UNDERLINE_MASK = 0b00001000; // Bit 3: Underline formatting
const STRIKE_MASK = 0b00010000; // Bit 4: Strikethrough formatting
const MENTION_MASK = 0b00100000; // Bit 5: Part of @mention
const INTEREST_MASK = 0b01000000; // Bit 6: Part of #hashtag
const LINK_MASK = 0b10000000; // Bit 7: Part of URL linkHow It Works
- Character Mapping: Each character position has a corresponding number in the bitmap array
- Bit Operations: Properties are set/unset using bitwise OR (
|) and AND (&) operations - Entity Building: Consecutive characters with the same bitmap value are grouped into entities
- Efficient Updates: Only modified bitmap sections trigger re-rendering
Example: The text "Hello @john" might have a bitmap like:
Text: H e l l o @ j o h n
Bitmap: 0 0 0 0 0 0 32 32 32 32 32Where 32 is 0b00100000 (MENTION_MASK), indicating those characters are part of a mention.
🚀 Quick Start
Installation
npm install @connectedxm/entity-editorDesign philosophy
The editor owns its own state during a message. Parent components do not mirror plain text, entities, or marks into their own React state. There are only two crossings between the editor and its parent:
- Mention search — the editor calls
onMentionSearchwhen the user types@foo; the parent renders a picker and callseditorRef.selectEntity(...)to inject the chosen result. - Submit time read — the parent calls
editorRef.getState()once, right before submitting, to get the final{ plainText, entities, markState }.
Auto-detected links (https://example.com) and hashtags (#bike) are tagged and styled inside the editor automatically. They surface in getState().entities at submit time — there is no per-event callback for these.
Basic Usage
import React, { useRef, useState } from "react";
import {
Editor,
EditorRef,
MarkState,
MentionSearch,
} from "@connectedxm/entity-editor";
function App() {
const editorRef = useRef<EditorRef>(null);
const [mentionSearch, setMentionSearch] = useState<MentionSearch | null>(null);
const [markState, setMarkState] = useState<MarkState>({
bold: false,
italic: false,
underline: false,
strike: false,
});
const handleSubmit = () => {
const state = editorRef.current?.getState();
if (!state) return;
// state.plainText -> "Hello @rob check #bike https://example.com"
// state.entities -> [{ type: "mention", ... }, { type: "interest", ... }, { type: "link", ... }]
// state.markState -> { bold: false, italic: false, underline: false, strike: false }
submitToBackend(state);
};
return (
<>
<Editor
ref={editorRef}
onMentionSearch={setMentionSearch}
onMarkStateChange={setMarkState}
options={{ mentions: true, interests: true, links: true }}
placeholder="Type something..."
style={{ border: "1px solid #ccc", minHeight: "100px", padding: "10px" }}
/>
{/* render your mention picker using `mentionSearch` */}
{/* render your toolbar buttons using `markState` */}
<button onClick={handleSubmit}>Send</button>
</>
);
}Mention picker round-trip
{mentionSearch && (
<ul>
{searchAccounts(mentionSearch.search).map((account) => (
<li
key={account.id}
onClick={() =>
editorRef.current?.selectEntity(
"mention",
mentionSearch.startIndex,
mentionSearch.endIndex,
account.username
)
}
>
{account.name} (@{account.username})
</li>
))}
</ul>
)}selectEntity replaces the in-progress @foo with the picked username, tags the bitmap as a mention, and clears the search (the next onMentionSearch invocation fires with null).
Toolbar
<button onClick={() => editorRef.current?.toggleMark("bold")}>
{markState.bold ? "✓ Bold" : "Bold"}
</button>The toolbar drives marks via toggleMark. The editor reflects the current cursor's mark state through onMarkStateChange so toolbar buttons can show their active state.
Initial content
<Editor
ref={editorRef}
initialPlainText="Welcome back @rob"
initialEntities={[
{ type: "mention", startIndex: 12, endIndex: 16, marks: [], username: "rob" },
]}
/>initialPlainText and initialEntities are read once on mount. To restore content imperatively later, call editorRef.current?.reset().
Styling Entities with CSS
Add CSS to style the recognized entities:
.activity-mention {
color: #1da1f2;
background-color: rgba(29, 161, 242, 0.1);
border-radius: 3px;
padding: 0 2px;
}
.activity-interest {
color: #ff6b35;
font-weight: 500;
}
.activity-link {
color: #9c27b0;
text-decoration: underline;
}
.activity-bold {
font-weight: bold;
}
.activity-italic {
font-style: italic;
}
.activity-underline {
text-decoration: underline;
}
.activity-strike {
text-decoration: line-through;
}📊 Entity Structure
The editor converts bitmap data into structured entities for easy consumption:
interface Entity {
type: "mention" | "interest" | "link" | "segment";
startIndex: number;
endIndex: number;
marks: ("bold" | "italic" | "underline" | "strike")[];
href?: string; // For links
username?: string; // For mentions
interest?: string; // For interests
}
interface SearchEntity {
type: "mention" | "interest" | "link";
search: string;
startIndex: number;
endIndex: number;
}Bitmap to Entity Conversion
- Bitmap Scanning: The system scans through the bitmap array
- Grouping: Consecutive characters with identical bit values are grouped together
- Entity Creation: Each group becomes an entity with its type determined by the bitmap value
- Mark Extraction: Formatting bits are converted to a
marksarray
🎯 Recognition Patterns
- Mentions:
@username(alphanumeric, underscores, hyphens, apostrophes) - Interests:
#hashtag(alphanumeric, underscores, hyphens) - Links:
https://orhttp://URLs - Formatting: Applied via keyboard shortcuts or toolbar
⚡ Performance Benefits
Why Bitmap Architecture?
The bitmap approach provides significant performance advantages:
Memory Efficiency:
- Each character: 1 number (4-8 bytes) vs complex objects (50+ bytes)
- 10,000 characters: ~40KB vs ~500KB+ in traditional approaches
- 90%+ memory reduction for large documents
Processing Speed:
- Bit operations are CPU-native and extremely fast
- O(1) property checks vs O(n) object property lookups
- 5-10x faster entity processing
Update Performance:
- Only modified bitmap regions trigger re-rendering
- Optimized React re-renders through efficient state management
📝 API Reference
Props
| Prop | Type | Required | Description |
| ------------------- | ------------------------------------------------- | -------- | ----------------------------------------------------------------------------- |
| ref | React.RefObject<EditorRef \| null> | Yes | Ref to access editor methods |
| initialPlainText | string | No | Initial text content (read once on mount) |
| initialEntities | Entity[] | No | Initial entities to restore (read once on mount) |
| onMentionSearch | (search: MentionSearch \| null) => void | No | Fires (debounced 300ms) when the user types/clears @foo |
| onMarkStateChange | (state: MarkState) => void | No | Fires when the cursor's bold/italic/underline/strike state changes |
| options | EntityOptions | No | Toggle mentions / interests / links detection |
| entityStyles | StyleOptions | No | Custom colors for entity types |
| placeholder | string | No | Placeholder text shown when empty and unfocused |
| style | React.CSSProperties | No | Inline styles for the editor |
| className | string | No | CSS class name for the editor |
| debug | boolean | No | Render the raw bitmap below the editor (default: false) |
Ref methods (EditorRef)
| Method | Signature | Purpose |
| -------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| getState | () => { plainText, entities, markState } | Submit-time read. Runs a final commit sweep then returns state. |
| selectEntity | (type: EntityType, startIndex: number, endIndex: number, newText: string) => void | Inject a picked mention/interest/link in place of the search word. |
| toggleMark | (mark: MarkType) => void | Apply / remove a mark on the current selection. |
| clear | () => void | Wipe content to empty. |
| reset | () => void | Restore the editor to initialPlainText / initialEntities. |
| focus | () => void | Move keyboard focus to the editor. |
Interfaces
interface EntityOptions {
mentions?: boolean; // Enable @mention detection
interests?: boolean; // Enable #hashtag detection
links?: boolean; // Enable URL detection
}
interface StyleOptions {
mentionColor?: string; // Custom color for mentions
interestColor?: string; // Custom color for interests
linkColor?: string; // Custom color for links
}
interface MarkState {
bold: boolean;
italic: boolean;
underline: boolean;
strike: boolean;
}
interface MentionSearch {
type: "mention";
search: string; // the text after @ (without the @)
startIndex: number; // position of the @ in plainText
endIndex: number; // position immediately after the search word
}
interface EditorState {
plainText: string;
entities: Entity[];
markState: MarkState;
}Keyboard Shortcuts
- Ctrl/Cmd + B: Toggle bold
- Ctrl/Cmd + I: Toggle italic
- Ctrl/Cmd + U: Toggle underline
- Ctrl/Cmd + Shift + S: Toggle strikethrough
🧪 Testing
Run the test suite:
npm test # Run tests in watch mode
npm run test:run # Run tests once🏗️ Development
# Install dependencies
npm install
# Start development server
npm run dev
# Build for production
npm run build
# Create local package
npm run local🏆 Why Zero Dependencies?
This editor proves that powerful text editing doesn't require heavy libraries:
- Performance: No bundle bloat, faster load times
- Security: No third-party vulnerabilities
- Control: Full understanding and control over every feature
- Maintenance: Easier updates and customization
- Reliability: No breaking changes from external dependencies
🤝 Contributing
This project maintains its zero-dependency philosophy and bitmap-based architecture. When contributing:
- No External Dependencies: Keep the runtime dependency-free
- Bitmap First: All new features should leverage the bitmap system
- Performance Focused: Optimize for memory usage and processing speed
- TypeScript: Maintain strict typing throughout
- Test Coverage: Add comprehensive tests for new features
- API Stability: Keep the component interface simple and focused
Understanding the Codebase
src/Editor.tsx- Main editor componentsrc/interfaces.ts- TypeScript interfaces and typessrc/helpers/bitmap.ts- Core bitmap manipulation functionssrc/helpers/entities.ts- Bitmap-to-entity conversion logicsrc/helpers/marks/- Individual formatting bit operationssrc/helpers/entities/- Entity type detection and processingsrc/helpers/dom.ts- DOM manipulation and cursor managementsrc/helpers/keyboard.ts- Keyboard shortcut handlingsrc/hooks/useDebounce.ts- Debounce hook for performance
📄 License
MIT License - feel free to use in your projects!
Built with ❤️ and zero dependencies by ConnectedXM
