ink-prompt 0.2.2 → 0.2.4

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.
Files changed (61) hide show
  1. package/README.md +2 -2
  2. package/dist/components/MultilineInput/AtomicBlocks.d.ts +10 -20
  3. package/dist/components/MultilineInput/AtomicBlocks.js +33 -42
  4. package/dist/components/MultilineInput/BlockMarker.d.ts +19 -0
  5. package/dist/components/MultilineInput/BlockMarker.js +83 -0
  6. package/dist/components/MultilineInput/BlockRegistry.d.ts +39 -0
  7. package/dist/components/MultilineInput/BlockRegistry.js +236 -0
  8. package/dist/components/MultilineInput/BlockTypes.d.ts +22 -0
  9. package/dist/components/MultilineInput/ImageTypes.d.ts +0 -2
  10. package/dist/components/MultilineInput/ImageTypes.js +1 -2
  11. package/dist/components/MultilineInput/ImageValidator.js +1 -1
  12. package/dist/components/MultilineInput/KeyHandler.d.ts +1 -1
  13. package/dist/components/MultilineInput/TextBuffer.d.ts +5 -31
  14. package/dist/components/MultilineInput/TextBuffer.js +91 -161
  15. package/dist/components/MultilineInput/TextRenderer.d.ts +6 -7
  16. package/dist/components/MultilineInput/TextRenderer.js +7 -7
  17. package/dist/components/MultilineInput/__tests__/BlockMarker.test.js +130 -0
  18. package/dist/components/MultilineInput/__tests__/BlockRegistry.test.js +225 -0
  19. package/dist/components/MultilineInput/__tests__/Placeholder.integration.test.js +44 -65
  20. package/dist/components/MultilineInput/__tests__/TextBuffer_images.test.js +10 -31
  21. package/dist/components/MultilineInput/__tests__/TextRenderer_images.test.js +27 -13
  22. package/dist/components/MultilineInput/__tests__/integration_images.test.js +2 -4
  23. package/dist/components/MultilineInput/__tests__/useTextInput_images.test.js +30 -29
  24. package/dist/components/MultilineInput/index.d.ts +6 -6
  25. package/dist/components/MultilineInput/index.js +56 -13
  26. package/dist/components/MultilineInput/types.d.ts +0 -20
  27. package/dist/components/MultilineInput/useTextInput.d.ts +4 -11
  28. package/dist/components/MultilineInput/useTextInput.js +79 -76
  29. package/package.json +1 -1
  30. package/dist/components/MultilineInput/Placeholder.d.ts +0 -30
  31. package/dist/components/MultilineInput/Placeholder.js +0 -200
  32. package/dist/components/MultilineInput/__tests__/Placeholder.test.js +0 -235
  33. package/dist/examples/examples/basic.js +0 -9
  34. package/dist/examples/src/components/MultilineInput/KeyHandler.d.ts +0 -15
  35. package/dist/examples/src/components/MultilineInput/KeyHandler.js +0 -97
  36. package/dist/examples/src/components/MultilineInput/TextBuffer.d.ts +0 -34
  37. package/dist/examples/src/components/MultilineInput/TextBuffer.js +0 -127
  38. package/dist/examples/src/components/MultilineInput/TextRenderer.d.ts +0 -24
  39. package/dist/examples/src/components/MultilineInput/TextRenderer.js +0 -72
  40. package/dist/examples/src/components/MultilineInput/__tests__/KeyHandler.test.js +0 -115
  41. package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.d.ts +0 -1
  42. package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.js +0 -254
  43. package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.d.ts +0 -1
  44. package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.js +0 -176
  45. package/dist/examples/src/components/MultilineInput/__tests__/integration.test.d.ts +0 -1
  46. package/dist/examples/src/components/MultilineInput/__tests__/integration.test.js +0 -71
  47. package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.d.ts +0 -1
  48. package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.js +0 -65
  49. package/dist/examples/src/components/MultilineInput/index.d.ts +0 -39
  50. package/dist/examples/src/components/MultilineInput/index.js +0 -82
  51. package/dist/examples/src/components/MultilineInput/types.d.ts +0 -55
  52. package/dist/examples/src/components/MultilineInput/types.js +0 -1
  53. package/dist/examples/src/components/MultilineInput/useTextInput.d.ts +0 -16
  54. package/dist/examples/src/components/MultilineInput/useTextInput.js +0 -82
  55. package/dist/examples/src/hello.test.d.ts +0 -1
  56. package/dist/examples/src/hello.test.js +0 -13
  57. package/dist/examples/src/index.d.ts +0 -2
  58. package/dist/examples/src/index.js +0 -2
  59. /package/dist/components/MultilineInput/{__tests__/Placeholder.test.d.ts → BlockTypes.js} +0 -0
  60. /package/dist/{examples/examples/basic.d.ts → components/MultilineInput/__tests__/BlockMarker.test.d.ts} +0 -0
  61. /package/dist/{examples/src/components/MultilineInput/__tests__/KeyHandler.test.d.ts → components/MultilineInput/__tests__/BlockRegistry.test.d.ts} +0 -0
package/README.md CHANGED
@@ -46,7 +46,7 @@ render(<App />);
46
46
  | `onBoundaryArrow` | `(direction: 'up' \| 'down' \| 'left' \| 'right') => void` | | Called when arrow key reaches a boundary |
47
47
  | `undoDebounceMs` | `number` | `200` | Milliseconds of inactivity to commit undo batch (`0` = disable) |
48
48
  | `pasteThreshold` | `number` | | Max paste length before text is replaced by a placeholder |
49
- | `formatPastePlaceholder` | `(id: number) => string` | | Custom placeholder display format |
49
+ | `formatPastePlaceholder` | `(displayNumber: number) => string` | | Custom placeholder display format (1-based) |
50
50
  | `enableImagePaste` | `boolean` | `false` | Enables image-aware Ctrl+V handling |
51
51
  | `images` | `ImageRef[]` | | Controlled image state for pasted images |
52
52
  | `onImagesChange` | `(images: ImageRef[]) => void` | | Called when images change |
@@ -77,7 +77,7 @@ replace the pasted content with a compact placeholder for cleaner display.
77
77
  <MultilineInput
78
78
  onSubmit={(value) => console.log(value)}
79
79
  pasteThreshold={200} // Text >200 chars becomes a placeholder
80
- formatPastePlaceholder={(id) => `[Pasted block #${id}]`} // Optional formatter
80
+ formatPastePlaceholder={(n) => `[Pasted block #${n}]`} // Optional formatter
81
81
  />
82
82
  ```
83
83
 
@@ -1,25 +1,15 @@
1
- import type { PlaceholderInfo } from './types.js';
2
- export type AtomicBlock = {
3
- kind: 'sentinel';
1
+ import type { BlockEntry, BlockKind } from './BlockTypes.js';
2
+ export interface AtomicBlock {
3
+ kind: BlockKind;
4
4
  id: string;
5
- displayNumber: number;
6
5
  start: number;
7
6
  end: number;
8
7
  displayWidth: number;
9
8
  displayText: string;
10
- } | {
11
- kind: 'placeholder';
12
- id: number;
13
- start: number;
14
- end: number;
15
- displayWidth: number;
16
- displayText: string;
17
- };
18
- export type Placeholders = Map<number, PlaceholderInfo> | undefined;
19
- export declare function findAtomicBlocks(line: string, placeholders?: Placeholders): AtomicBlock[];
20
- /** Block that strictly contains offset (offset > start && offset < end) — cursor is in the interior. */
21
- export declare function findAtomicBlockSpanning(line: string, offset: number, placeholders?: Placeholders): AtomicBlock | null;
22
- /** Block whose end === offset (the block immediately to the left of the cursor). */
23
- export declare function findAtomicBlockBefore(line: string, offset: number, placeholders?: Placeholders): AtomicBlock | null;
24
- /** Block whose start === offset (the block immediately to the right of the cursor). */
25
- export declare function findAtomicBlockAfter(line: string, offset: number, placeholders?: Placeholders): AtomicBlock | null;
9
+ dim: boolean;
10
+ }
11
+ export type BlockEntries = Map<string, BlockEntry> | undefined;
12
+ export declare function findAtomicBlocks(line: string, entries?: BlockEntries): AtomicBlock[];
13
+ export declare function findAtomicBlockSpanning(line: string, offset: number, entries?: BlockEntries): AtomicBlock | null;
14
+ export declare function findAtomicBlockBefore(line: string, offset: number, entries?: BlockEntries): AtomicBlock | null;
15
+ export declare function findAtomicBlockAfter(line: string, offset: number, entries?: BlockEntries): AtomicBlock | null;
@@ -1,58 +1,49 @@
1
- import { parseSentinels, getPlaceholderText } from './ImageSentinel.js';
2
- import { MARKER_REGEX } from './Placeholder.js';
3
- export function findAtomicBlocks(line, placeholders) {
4
- const blocks = [];
5
- for (const s of parseSentinels(line)) {
6
- const displayText = getPlaceholderText(s.displayNumber);
7
- blocks.push({
8
- kind: 'sentinel',
9
- id: s.id,
10
- displayNumber: s.displayNumber,
11
- start: s.start,
12
- end: s.end,
13
- displayWidth: displayText.length,
14
- displayText,
15
- });
16
- }
17
- if (placeholders && placeholders.size > 0) {
18
- MARKER_REGEX.lastIndex = 0;
19
- let m;
20
- while ((m = MARKER_REGEX.exec(line)) !== null) {
21
- const id = Number(m[1]);
22
- const info = placeholders.get(id);
23
- const displayText = info ? info.displayText : '';
24
- blocks.push({
25
- kind: 'placeholder',
26
- id,
27
- start: m.index,
28
- end: m.index + m[0].length,
29
- displayWidth: displayText.length,
30
- displayText,
31
- });
1
+ import { parseBlockMarkers } from './BlockMarker.js';
2
+ function getDisplayInfo(marker, entries) {
3
+ if (entries) {
4
+ const entry = entries.get(marker.id);
5
+ if (entry && entry.kind === 'paste') {
6
+ return { displayWidth: entry.displayText.length, displayText: entry.displayText };
32
7
  }
33
8
  }
34
- blocks.sort((a, b) => a.start - b.start);
35
- return blocks;
9
+ if (marker.kind === 'p') {
10
+ const text = `[Paste text #${marker.displayNumber}]`;
11
+ return { displayWidth: text.length, displayText: text };
12
+ }
13
+ const text = `[Pasted Image #${marker.displayNumber}]`;
14
+ return { displayWidth: text.length, displayText: text };
15
+ }
16
+ export function findAtomicBlocks(line, entries) {
17
+ const markers = parseBlockMarkers(line);
18
+ return markers.map((m) => {
19
+ const { displayWidth, displayText } = getDisplayInfo(m, entries);
20
+ return {
21
+ kind: m.kind === 'p' ? 'paste' : 'image',
22
+ id: m.id,
23
+ start: m.start,
24
+ end: m.end,
25
+ displayWidth,
26
+ displayText,
27
+ dim: m.kind === 'i',
28
+ };
29
+ });
36
30
  }
37
- /** Block that strictly contains offset (offset > start && offset < end) — cursor is in the interior. */
38
- export function findAtomicBlockSpanning(line, offset, placeholders) {
39
- for (const b of findAtomicBlocks(line, placeholders)) {
31
+ export function findAtomicBlockSpanning(line, offset, entries) {
32
+ for (const b of findAtomicBlocks(line, entries)) {
40
33
  if (offset > b.start && offset < b.end)
41
34
  return b;
42
35
  }
43
36
  return null;
44
37
  }
45
- /** Block whose end === offset (the block immediately to the left of the cursor). */
46
- export function findAtomicBlockBefore(line, offset, placeholders) {
47
- for (const b of findAtomicBlocks(line, placeholders)) {
38
+ export function findAtomicBlockBefore(line, offset, entries) {
39
+ for (const b of findAtomicBlocks(line, entries)) {
48
40
  if (b.end === offset)
49
41
  return b;
50
42
  }
51
43
  return null;
52
44
  }
53
- /** Block whose start === offset (the block immediately to the right of the cursor). */
54
- export function findAtomicBlockAfter(line, offset, placeholders) {
55
- for (const b of findAtomicBlocks(line, placeholders)) {
45
+ export function findAtomicBlockAfter(line, offset, entries) {
46
+ for (const b of findAtomicBlocks(line, entries)) {
56
47
  if (b.start === offset)
57
48
  return b;
58
49
  }
@@ -0,0 +1,19 @@
1
+ export declare const BLOCK_OPEN = "\uE000";
2
+ export declare const BLOCK_CLOSE = "\uE001";
3
+ export type BlockMarkerKind = 'p' | 'i';
4
+ export interface BlockMarkerInfo {
5
+ kind: BlockMarkerKind;
6
+ id: string;
7
+ displayNumber: number;
8
+ start: number;
9
+ end: number;
10
+ }
11
+ export declare function generateBlockId(): string;
12
+ export declare function createBlockMarker(kind: BlockMarkerKind, id: string, displayNumber: number): string;
13
+ export declare function parseBlockMarkers(text: string): BlockMarkerInfo[];
14
+ export declare function findBlockMarkerAt(text: string, offset: number): BlockMarkerInfo | null;
15
+ export declare function findBlockMarkerBefore(text: string, offset: number): BlockMarkerInfo | null;
16
+ export declare function findBlockMarkerAfter(text: string, offset: number): BlockMarkerInfo | null;
17
+ export declare function removeBlockMarker(text: string, offset: number): string;
18
+ export declare function blockMarkerVisualWidth(displayNumber: number): number;
19
+ export declare function getBlockPlaceholderText(kind: BlockMarkerKind, displayNumber: number): string;
@@ -0,0 +1,83 @@
1
+ export const BLOCK_OPEN = '\uE000';
2
+ export const BLOCK_CLOSE = '\uE001';
3
+ export function generateBlockId() {
4
+ return Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 6);
5
+ }
6
+ export function createBlockMarker(kind, id, displayNumber) {
7
+ return `${BLOCK_OPEN}${kind}:${id}:${displayNumber}${BLOCK_CLOSE}`;
8
+ }
9
+ export function parseBlockMarkers(text) {
10
+ const result = [];
11
+ let i = 0;
12
+ while (i < text.length) {
13
+ const openIdx = text.indexOf(BLOCK_OPEN, i);
14
+ if (openIdx === -1)
15
+ break;
16
+ const closeIdx = text.indexOf(BLOCK_CLOSE, openIdx + 1);
17
+ if (closeIdx === -1)
18
+ break;
19
+ const raw = text.substring(openIdx + 1, closeIdx);
20
+ const kindChar = raw[0];
21
+ if (kindChar !== 'p' && kindChar !== 'i') {
22
+ i = closeIdx + 1;
23
+ continue;
24
+ }
25
+ const rest = raw.substring(2);
26
+ const colonIdx = rest.lastIndexOf(':');
27
+ if (colonIdx === -1) {
28
+ i = closeIdx + 1;
29
+ continue;
30
+ }
31
+ const id = rest.substring(0, colonIdx);
32
+ const displayNumber = parseInt(rest.substring(colonIdx + 1), 10);
33
+ if (isNaN(displayNumber)) {
34
+ i = closeIdx + 1;
35
+ continue;
36
+ }
37
+ result.push({
38
+ kind: kindChar,
39
+ id,
40
+ displayNumber,
41
+ start: openIdx,
42
+ end: closeIdx + 1,
43
+ });
44
+ i = closeIdx + 1;
45
+ }
46
+ return result;
47
+ }
48
+ export function findBlockMarkerAt(text, offset) {
49
+ for (const m of parseBlockMarkers(text)) {
50
+ if (offset > m.start && offset < m.end)
51
+ return m;
52
+ }
53
+ return null;
54
+ }
55
+ export function findBlockMarkerBefore(text, offset) {
56
+ for (const m of parseBlockMarkers(text)) {
57
+ if (m.end === offset)
58
+ return m;
59
+ }
60
+ return null;
61
+ }
62
+ export function findBlockMarkerAfter(text, offset) {
63
+ for (const m of parseBlockMarkers(text)) {
64
+ if (m.start === offset)
65
+ return m;
66
+ }
67
+ return null;
68
+ }
69
+ export function removeBlockMarker(text, offset) {
70
+ const marker = findBlockMarkerAt(text, offset);
71
+ if (!marker)
72
+ return text;
73
+ return text.slice(0, marker.start) + text.slice(marker.end);
74
+ }
75
+ export function blockMarkerVisualWidth(displayNumber) {
76
+ return getBlockPlaceholderText('i', displayNumber).length;
77
+ }
78
+ export function getBlockPlaceholderText(kind, displayNumber) {
79
+ if (kind === 'i') {
80
+ return `[Pasted Image #${displayNumber}]`;
81
+ }
82
+ return `[Paste text #${displayNumber}]`;
83
+ }
@@ -0,0 +1,39 @@
1
+ import type { BlockEntry, BlockState } from './BlockTypes.js';
2
+ import { type BlockMarkerKind } from './BlockMarker.js';
3
+ import type { ImageRef } from './ImageTypes.js';
4
+ export declare function createBlockState(): BlockState;
5
+ export declare function createPasteBlockEntry(state: BlockState, originalText: string, displayText: string): {
6
+ id: string;
7
+ marker: string;
8
+ state: BlockState;
9
+ };
10
+ export declare function createImageBlockEntry(state: BlockState, imageRef: ImageRef, id?: string): {
11
+ id: string;
12
+ marker: string;
13
+ state: BlockState;
14
+ };
15
+ export declare function removeBlock(state: BlockState, id: string): BlockState;
16
+ export declare function getBlock(state: BlockState, id: string): BlockEntry | undefined;
17
+ export declare function getDisplayLine(line: string, entries: Map<string, BlockEntry>): string;
18
+ export declare function getValue(lines: string[], entries: Map<string, BlockEntry>): string;
19
+ export declare function bufferColToDisplayCol(line: string, column: number, entries: Map<string, BlockEntry>): number;
20
+ export declare function displayColToBufferCol(line: string, displayColumn: number, entries: Map<string, BlockEntry>): number;
21
+ export declare function getExpandedLineLength(line: string, entries: Map<string, BlockEntry>): number;
22
+ export declare function getValueCursorOffset(lines: string[], cursor: {
23
+ line: number;
24
+ column: number;
25
+ }, entries: Map<string, BlockEntry>): number;
26
+ export declare function getCursorFromValueOffset(lines: string[], offset: number, entries: Map<string, BlockEntry>): {
27
+ line: number;
28
+ column: number;
29
+ };
30
+ export declare function getDisplayWidthForMarker(m: {
31
+ kind: BlockMarkerKind;
32
+ id: string;
33
+ displayNumber: number;
34
+ }, entries: Map<string, BlockEntry>): number;
35
+ export declare function getDisplayTextForMarker(m: {
36
+ kind: BlockMarkerKind;
37
+ id: string;
38
+ displayNumber: number;
39
+ }, entries: Map<string, BlockEntry>): string;
@@ -0,0 +1,236 @@
1
+ import { createBlockMarker, generateBlockId, parseBlockMarkers, } from './BlockMarker.js';
2
+ export function createBlockState() {
3
+ return { entries: new Map(), nextPasteNumber: 1, nextImageNumber: 1 };
4
+ }
5
+ export function createPasteBlockEntry(state, originalText, displayText) {
6
+ const id = generateBlockId();
7
+ const displayNumber = state.nextPasteNumber;
8
+ const marker = createBlockMarker('p', id, displayNumber);
9
+ const newEntries = new Map(state.entries);
10
+ newEntries.set(id, { kind: 'paste', id, displayNumber, originalText, displayText });
11
+ return {
12
+ id,
13
+ marker,
14
+ state: { entries: newEntries, nextPasteNumber: displayNumber + 1, nextImageNumber: state.nextImageNumber },
15
+ };
16
+ }
17
+ export function createImageBlockEntry(state, imageRef, id) {
18
+ const blockId = id || imageRef.id;
19
+ const displayNumber = imageRef.displayNumber;
20
+ const marker = createBlockMarker('i', blockId, displayNumber);
21
+ const newEntries = new Map(state.entries);
22
+ newEntries.set(blockId, {
23
+ kind: 'image',
24
+ id: blockId,
25
+ displayNumber,
26
+ data: imageRef.data,
27
+ mimeType: imageRef.mimeType,
28
+ byteSize: imageRef.byteSize,
29
+ });
30
+ return {
31
+ id: blockId,
32
+ marker,
33
+ state: {
34
+ entries: newEntries,
35
+ nextPasteNumber: state.nextPasteNumber,
36
+ nextImageNumber: Math.max(state.nextImageNumber, displayNumber + 1),
37
+ },
38
+ };
39
+ }
40
+ export function removeBlock(state, id) {
41
+ const newEntries = new Map(state.entries);
42
+ newEntries.delete(id);
43
+ return { ...state, entries: newEntries };
44
+ }
45
+ export function getBlock(state, id) {
46
+ return state.entries.get(id);
47
+ }
48
+ export function getDisplayLine(line, entries) {
49
+ const markers = parseBlockMarkers(line);
50
+ if (markers.length === 0)
51
+ return line;
52
+ let result = '';
53
+ let lastEnd = 0;
54
+ for (const m of markers) {
55
+ result += line.slice(lastEnd, m.start);
56
+ const entry = entries.get(m.id);
57
+ if (entry && entry.kind === 'paste') {
58
+ result += entry.displayText;
59
+ }
60
+ else {
61
+ result += m.kind === 'i' ? `[Pasted Image #${m.displayNumber}]` : `[Paste text #${m.displayNumber}]`;
62
+ }
63
+ lastEnd = m.end;
64
+ }
65
+ result += line.slice(lastEnd);
66
+ return result;
67
+ }
68
+ export function getValue(lines, entries) {
69
+ return lines.map((line) => {
70
+ const markers = parseBlockMarkers(line);
71
+ if (markers.length === 0)
72
+ return line;
73
+ let result = '';
74
+ let lastEnd = 0;
75
+ for (const m of markers) {
76
+ result += line.slice(lastEnd, m.start);
77
+ const entry = entries.get(m.id);
78
+ if (entry && entry.kind === 'paste') {
79
+ result += entry.originalText;
80
+ }
81
+ else {
82
+ result += line.slice(m.start, m.end);
83
+ }
84
+ lastEnd = m.end;
85
+ }
86
+ result += line.slice(lastEnd);
87
+ return result;
88
+ }).join('\n');
89
+ }
90
+ export function bufferColToDisplayCol(line, column, entries) {
91
+ if (entries.size === 0)
92
+ return column;
93
+ let displayCol = 0;
94
+ let lastEnd = 0;
95
+ const markers = parseBlockMarkers(line);
96
+ for (const m of markers) {
97
+ if (column <= m.start) {
98
+ return displayCol + (column - lastEnd);
99
+ }
100
+ displayCol += m.start - lastEnd;
101
+ const entry = entries.get(m.id);
102
+ const displayLen = entry && entry.kind === 'paste' ? entry.displayText.length : `[Pasted Image #${m.displayNumber}]`.length;
103
+ if (column <= m.end) {
104
+ return displayCol + displayLen;
105
+ }
106
+ displayCol += displayLen;
107
+ lastEnd = m.end;
108
+ }
109
+ return displayCol + (column - lastEnd);
110
+ }
111
+ export function displayColToBufferCol(line, displayColumn, entries) {
112
+ if (entries.size === 0)
113
+ return displayColumn;
114
+ let bufPos = 0;
115
+ let dispPos = 0;
116
+ const markers = parseBlockMarkers(line);
117
+ for (const m of markers) {
118
+ const textLen = m.start - bufPos;
119
+ if (displayColumn <= dispPos + textLen) {
120
+ return bufPos + (displayColumn - dispPos);
121
+ }
122
+ dispPos += textLen;
123
+ bufPos = m.start;
124
+ const entry = entries.get(m.id);
125
+ const displayLen = entry && entry.kind === 'paste' ? entry.displayText.length : `[Pasted Image #${m.displayNumber}]`.length;
126
+ if (displayColumn <= dispPos + displayLen) {
127
+ return m.end;
128
+ }
129
+ dispPos += displayLen;
130
+ bufPos = m.end;
131
+ }
132
+ return bufPos + (displayColumn - dispPos);
133
+ }
134
+ export function getExpandedLineLength(line, entries) {
135
+ let len = 0;
136
+ let lastEnd = 0;
137
+ const markers = parseBlockMarkers(line);
138
+ for (const m of markers) {
139
+ len += m.start - lastEnd;
140
+ const entry = entries.get(m.id);
141
+ if (entry && entry.kind === 'paste') {
142
+ len += entry.originalText.length;
143
+ }
144
+ else {
145
+ len += m.end - m.start;
146
+ }
147
+ lastEnd = m.end;
148
+ }
149
+ len += line.length - lastEnd;
150
+ return len;
151
+ }
152
+ export function getValueCursorOffset(lines, cursor, entries) {
153
+ let offset = 0;
154
+ for (let i = 0; i < cursor.line; i++) {
155
+ offset += getExpandedLineLength(lines[i], entries) + 1;
156
+ }
157
+ const line = lines[cursor.line];
158
+ let bufPos = 0;
159
+ const markers = parseBlockMarkers(line);
160
+ for (const m of markers) {
161
+ if (cursor.column <= m.start) {
162
+ offset += cursor.column - bufPos;
163
+ return offset;
164
+ }
165
+ offset += m.start - bufPos;
166
+ const entry = entries.get(m.id);
167
+ if (entry && entry.kind === 'paste') {
168
+ offset += entry.originalText.length;
169
+ }
170
+ else {
171
+ offset += m.end - m.start;
172
+ }
173
+ bufPos = m.end;
174
+ if (cursor.column <= m.end) {
175
+ return offset;
176
+ }
177
+ }
178
+ offset += cursor.column - bufPos;
179
+ return offset;
180
+ }
181
+ export function getCursorFromValueOffset(lines, offset, entries) {
182
+ let currentOffset = 0;
183
+ const lineCount = lines.length;
184
+ for (let i = 0; i < lineCount; i++) {
185
+ const lineLen = getExpandedLineLength(lines[i], entries);
186
+ if (i === lineCount - 1) {
187
+ if (offset <= currentOffset + lineLen) {
188
+ const colInExpanded = offset - currentOffset;
189
+ return { line: i, column: valueOffsetToBufferColumn(lines[i], colInExpanded, entries) };
190
+ }
191
+ return { line: i, column: lines[i].length };
192
+ }
193
+ if (offset <= currentOffset + lineLen) {
194
+ const colInExpanded = offset - currentOffset;
195
+ return { line: i, column: valueOffsetToBufferColumn(lines[i], colInExpanded, entries) };
196
+ }
197
+ currentOffset += lineLen + 1;
198
+ }
199
+ const lastIdx = lines.length - 1;
200
+ return { line: lastIdx, column: lines[lastIdx].length };
201
+ }
202
+ function valueOffsetToBufferColumn(line, offsetInExpanded, entries) {
203
+ let bufPos = 0;
204
+ let expandedPos = 0;
205
+ const markers = parseBlockMarkers(line);
206
+ for (const m of markers) {
207
+ const textLen = m.start - bufPos;
208
+ if (offsetInExpanded <= expandedPos + textLen) {
209
+ return bufPos + (offsetInExpanded - expandedPos);
210
+ }
211
+ expandedPos += textLen;
212
+ bufPos = m.start;
213
+ const entry = entries.get(m.id);
214
+ const originalLen = entry && entry.kind === 'paste' ? entry.originalText.length : (m.end - m.start);
215
+ if (offsetInExpanded <= expandedPos + originalLen) {
216
+ return m.end;
217
+ }
218
+ expandedPos += originalLen;
219
+ bufPos = m.end;
220
+ }
221
+ return bufPos + (offsetInExpanded - expandedPos);
222
+ }
223
+ export function getDisplayWidthForMarker(m, entries) {
224
+ const entry = entries.get(m.id);
225
+ if (entry && entry.kind === 'paste') {
226
+ return entry.displayText.length;
227
+ }
228
+ return `[Pasted Image #${m.displayNumber}]`.length;
229
+ }
230
+ export function getDisplayTextForMarker(m, entries) {
231
+ const entry = entries.get(m.id);
232
+ if (entry && entry.kind === 'paste') {
233
+ return entry.displayText;
234
+ }
235
+ return `[Pasted Image #${m.displayNumber}]`;
236
+ }
@@ -0,0 +1,22 @@
1
+ export type BlockKind = 'paste' | 'image';
2
+ export interface PasteBlockEntry {
3
+ kind: 'paste';
4
+ id: string;
5
+ displayNumber: number;
6
+ originalText: string;
7
+ displayText: string;
8
+ }
9
+ export interface ImageBlockEntry {
10
+ kind: 'image';
11
+ id: string;
12
+ displayNumber: number;
13
+ data: string;
14
+ mimeType: string;
15
+ byteSize: number;
16
+ }
17
+ export type BlockEntry = PasteBlockEntry | ImageBlockEntry;
18
+ export interface BlockState {
19
+ entries: Map<string, BlockEntry>;
20
+ nextPasteNumber: number;
21
+ nextImageNumber: number;
22
+ }
@@ -6,5 +6,3 @@ export interface ImageRef {
6
6
  displayNumber: number;
7
7
  }
8
8
  export type PasteErrorReason = 'clipboard-timeout' | 'clipboard-read-error' | 'clipboard-unsupported-type' | 'image-too-large' | 'too-many-images' | 'clipboard-empty';
9
- export declare const SENTINEL_OPEN = "\uE000";
10
- export declare const SENTINEL_CLOSE = "\uE001";
@@ -1,2 +1 @@
1
- export const SENTINEL_OPEN = '\uE000';
2
- export const SENTINEL_CLOSE = '\uE001';
1
+ export {};
@@ -1,4 +1,4 @@
1
- import { generateImageId } from './ImageSentinel.js';
1
+ import { generateBlockId as generateImageId } from './BlockMarker.js';
2
2
  const MAGIC_BYTES = [
3
3
  { mimeType: 'image/png', magic: [0x89, 0x50, 0x4e, 0x47] },
4
4
  { mimeType: 'image/jpeg', magic: [0xff, 0xd8, 0xff] },
@@ -1,6 +1,6 @@
1
1
  import { type Key, type Buffer, type Cursor } from './types.js';
2
2
  import { type UseTextInputResult } from './useTextInput.js';
3
- export interface KeyHandlerActions extends Omit<UseTextInputResult, 'value' | 'cursor' | 'cursorOffset' | 'setCursorOffset' | 'buffer' | 'placeholderState' | 'insertImage' | 'images' | 'getImages' | 'setImages'> {
3
+ export interface KeyHandlerActions extends Omit<UseTextInputResult, 'value' | 'cursor' | 'cursorOffset' | 'setCursorOffset' | 'buffer' | 'blockState' | 'insertImage' | 'images' | 'getImages' | 'setImages'> {
4
4
  submit: () => void;
5
5
  onBoundaryArrow?: (direction: 'up' | 'down' | 'left' | 'right') => void;
6
6
  paste?: () => void;
@@ -1,5 +1,5 @@
1
1
  import type { Buffer, Cursor, Direction } from './types.js';
2
- import { type Placeholders } from './AtomicBlocks.js';
2
+ import { type BlockEntries } from './AtomicBlocks.js';
3
3
  /**
4
4
  * Create a new buffer from optional initial text
5
5
  */
@@ -15,14 +15,14 @@ export declare function insertText(buffer: Buffer, cursor: Cursor, text: string)
15
15
  /**
16
16
  * Delete character before cursor (backspace)
17
17
  */
18
- export declare function deleteChar(buffer: Buffer, cursor: Cursor, placeholders?: Placeholders): {
18
+ export declare function deleteChar(buffer: Buffer, cursor: Cursor, entries?: BlockEntries): {
19
19
  buffer: Buffer;
20
20
  cursor: Cursor;
21
21
  };
22
22
  /**
23
23
  * Delete character after cursor (forward delete / Delete key)
24
24
  */
25
- export declare function deleteCharForward(buffer: Buffer, cursor: Cursor, placeholders?: Placeholders): {
25
+ export declare function deleteCharForward(buffer: Buffer, cursor: Cursor, entries?: BlockEntries): {
26
26
  buffer: Buffer;
27
27
  cursor: Cursor;
28
28
  };
@@ -33,39 +33,13 @@ export declare function insertNewLine(buffer: Buffer, cursor: Cursor): {
33
33
  buffer: Buffer;
34
34
  cursor: Cursor;
35
35
  };
36
- /**
37
- * Information about a visual row within a wrapped line.
38
- */
39
36
  interface VisualRowInfo {
40
- /** Starting offset in the buffer line */
41
37
  start: number;
42
- /** Length of this visual row */
43
38
  length: number;
44
39
  }
45
- /**
46
- * Break a line into visual rows using word-aware wrapping.
47
- * Words are kept intact when possible, breaking at spaces.
48
- * Long words that exceed width are hard-wrapped.
49
- * Sentinel blocks are atomic: never split, and occupy visual width
50
- * equal to their placeholder text length.
51
- */
52
- export declare function getVisualRows(line: string, width: number, placeholders?: Placeholders): VisualRowInfo[];
53
- /**
54
- * Move cursor in specified direction with bounds checking.
55
- * When width is provided, up/down movement is based on visual lines (accounting for wrapping).
56
- * When width is not provided, up/down movement is based on buffer lines.
57
- */
58
- export declare function moveCursor(buffer: Buffer, cursor: Cursor, direction: Direction, width?: number, placeholders?: Placeholders): Cursor;
59
- /**
60
- * Get the full text content from buffer (lines joined with newlines)
61
- */
40
+ export declare function getVisualRows(line: string, width: number, entries?: BlockEntries): VisualRowInfo[];
41
+ export declare function moveCursor(buffer: Buffer, cursor: Cursor, direction: Direction, width?: number, entries?: BlockEntries): Cursor;
62
42
  export declare function getTextContent(buffer: Buffer): string;
63
- /**
64
- * Get the flat offset (index) from a cursor position.
65
- */
66
43
  export declare function getOffset(buffer: Buffer, cursor: Cursor): number;
67
- /**
68
- * Get the cursor position from a flat offset.
69
- */
70
44
  export declare function getCursor(buffer: Buffer, offset: number): Cursor;
71
45
  export {};