@domenico-esposito/react-native-markdown-editor 0.1.1

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 (76) hide show
  1. package/.eslintrc.js +5 -0
  2. package/README.md +265 -0
  3. package/build/MarkdownRenderer.d.ts +12 -0
  4. package/build/MarkdownRenderer.d.ts.map +1 -0
  5. package/build/MarkdownRenderer.js +165 -0
  6. package/build/MarkdownRenderer.js.map +1 -0
  7. package/build/MarkdownTextInput.d.ts +10 -0
  8. package/build/MarkdownTextInput.d.ts.map +1 -0
  9. package/build/MarkdownTextInput.js +233 -0
  10. package/build/MarkdownTextInput.js.map +1 -0
  11. package/build/MarkdownToolbar.d.ts +11 -0
  12. package/build/MarkdownToolbar.d.ts.map +1 -0
  13. package/build/MarkdownToolbar.js +98 -0
  14. package/build/MarkdownToolbar.js.map +1 -0
  15. package/build/index.d.ts +14 -0
  16. package/build/index.d.ts.map +1 -0
  17. package/build/index.js +11 -0
  18. package/build/index.js.map +1 -0
  19. package/build/markdownCore.types.d.ts +321 -0
  20. package/build/markdownCore.types.d.ts.map +1 -0
  21. package/build/markdownCore.types.js +2 -0
  22. package/build/markdownCore.types.js.map +1 -0
  23. package/build/markdownHighlight.d.ts +31 -0
  24. package/build/markdownHighlight.d.ts.map +1 -0
  25. package/build/markdownHighlight.js +378 -0
  26. package/build/markdownHighlight.js.map +1 -0
  27. package/build/markdownHighlight.types.d.ts +48 -0
  28. package/build/markdownHighlight.types.d.ts.map +1 -0
  29. package/build/markdownHighlight.types.js +9 -0
  30. package/build/markdownHighlight.types.js.map +1 -0
  31. package/build/markdownParser.d.ts +16 -0
  32. package/build/markdownParser.d.ts.map +1 -0
  33. package/build/markdownParser.js +309 -0
  34. package/build/markdownParser.js.map +1 -0
  35. package/build/markdownRendererDefaults.d.ts +113 -0
  36. package/build/markdownRendererDefaults.d.ts.map +1 -0
  37. package/build/markdownRendererDefaults.js +174 -0
  38. package/build/markdownRendererDefaults.js.map +1 -0
  39. package/build/markdownSegment.types.d.ts +22 -0
  40. package/build/markdownSegment.types.d.ts.map +1 -0
  41. package/build/markdownSegment.types.js +2 -0
  42. package/build/markdownSegment.types.js.map +1 -0
  43. package/build/markdownSegmentDefaults.d.ts +43 -0
  44. package/build/markdownSegmentDefaults.d.ts.map +1 -0
  45. package/build/markdownSegmentDefaults.js +176 -0
  46. package/build/markdownSegmentDefaults.js.map +1 -0
  47. package/build/markdownSyntaxUtils.d.ts +58 -0
  48. package/build/markdownSyntaxUtils.d.ts.map +1 -0
  49. package/build/markdownSyntaxUtils.js +98 -0
  50. package/build/markdownSyntaxUtils.js.map +1 -0
  51. package/build/markdownToolbarActions.d.ts +12 -0
  52. package/build/markdownToolbarActions.d.ts.map +1 -0
  53. package/build/markdownToolbarActions.js +212 -0
  54. package/build/markdownToolbarActions.js.map +1 -0
  55. package/build/useMarkdownEditor.d.ts +10 -0
  56. package/build/useMarkdownEditor.d.ts.map +1 -0
  57. package/build/useMarkdownEditor.js +219 -0
  58. package/build/useMarkdownEditor.js.map +1 -0
  59. package/jest.config.js +10 -0
  60. package/package.json +45 -0
  61. package/src/MarkdownRenderer.tsx +240 -0
  62. package/src/MarkdownTextInput.tsx +263 -0
  63. package/src/MarkdownToolbar.tsx +126 -0
  64. package/src/index.ts +31 -0
  65. package/src/markdownCore.types.ts +405 -0
  66. package/src/markdownHighlight.ts +413 -0
  67. package/src/markdownHighlight.types.ts +75 -0
  68. package/src/markdownParser.ts +345 -0
  69. package/src/markdownRendererDefaults.tsx +207 -0
  70. package/src/markdownSegment.types.ts +24 -0
  71. package/src/markdownSegmentDefaults.tsx +208 -0
  72. package/src/markdownSyntaxUtils.ts +139 -0
  73. package/src/markdownToolbarActions.ts +296 -0
  74. package/src/useMarkdownEditor.ts +265 -0
  75. package/tsconfig.json +9 -0
  76. package/tsconfig.test.json +8 -0
@@ -0,0 +1,212 @@
1
+ export const DEFAULT_MARKDOWN_FEATURES = [
2
+ 'bold',
3
+ 'italic',
4
+ 'strikethrough',
5
+ 'code',
6
+ 'codeBlock',
7
+ 'heading1',
8
+ 'heading2',
9
+ 'heading3',
10
+ 'heading4',
11
+ 'heading5',
12
+ 'heading6',
13
+ 'quote',
14
+ 'unorderedList',
15
+ 'orderedList',
16
+ 'divider',
17
+ 'image',
18
+ ];
19
+ const INLINE_ACTION_MARKERS = {
20
+ bold: { open: '**', close: '**' },
21
+ italic: { open: '_', close: '_' },
22
+ strikethrough: { open: '~~', close: '~~' },
23
+ code: { open: '`', close: '`' },
24
+ };
25
+ export function applyMarkdownToolbarAction({ action, text, selection, activeInlineActions = [], }) {
26
+ if (action === 'image') {
27
+ return applyImageAction({ text, selection, activeInlineActions });
28
+ }
29
+ if (isInlineToolbarAction(action)) {
30
+ return applyInlineAction({
31
+ action,
32
+ text,
33
+ selection,
34
+ activeInlineActions,
35
+ });
36
+ }
37
+ return applyBlockAction({
38
+ action,
39
+ text,
40
+ selection,
41
+ activeInlineActions,
42
+ });
43
+ }
44
+ /**
45
+ * Wraps or unwraps the selected text (or inserts markers at cursor)
46
+ * for an inline formatting action (bold, italic, strikethrough, code).
47
+ */
48
+ function applyInlineAction({ action, text, selection, activeInlineActions, }) {
49
+ const marker = INLINE_ACTION_MARKERS[action];
50
+ if (!marker) {
51
+ return {
52
+ text,
53
+ selection,
54
+ activeInlineActions,
55
+ };
56
+ }
57
+ if (selection.start !== selection.end) {
58
+ const selectedText = text.slice(selection.start, selection.end);
59
+ const nextText = `${text.slice(0, selection.start)}${marker.open}${selectedText}${marker.close}${text.slice(selection.end)}`;
60
+ const markerSize = marker.open.length;
61
+ return {
62
+ text: nextText,
63
+ selection: {
64
+ start: selection.start + markerSize,
65
+ end: selection.end + markerSize,
66
+ },
67
+ activeInlineActions: activeInlineActions.filter((value) => value !== action),
68
+ };
69
+ }
70
+ const cursor = selection.start;
71
+ const isActive = activeInlineActions.includes(action);
72
+ const nextMarker = isActive ? marker.close : marker.open;
73
+ const nextText = `${text.slice(0, cursor)}${nextMarker}${text.slice(cursor)}`;
74
+ const nextSelection = {
75
+ start: cursor + nextMarker.length,
76
+ end: cursor + nextMarker.length,
77
+ };
78
+ const nextActiveInlineActions = isActive
79
+ ? activeInlineActions.filter((value) => value !== action)
80
+ : [...activeInlineActions, action];
81
+ return {
82
+ text: nextText,
83
+ selection: nextSelection,
84
+ activeInlineActions: nextActiveInlineActions,
85
+ };
86
+ }
87
+ /**
88
+ * Applies a block-level toolbar action (heading, quote, list, divider, code block).
89
+ * Identifies the lines covered by the current selection and transforms them.
90
+ */
91
+ function applyBlockAction({ action, text, selection, activeInlineActions, }) {
92
+ const lineRange = getSelectedLineRange(text, selection);
93
+ const lines = lineRange.content.split('\n');
94
+ const nextLines = transformLinesByAction(action, lines);
95
+ const nextContent = nextLines.join('\n');
96
+ const nextText = `${text.slice(0, lineRange.start)}${nextContent}${text.slice(lineRange.end)}`;
97
+ if (selection.start === selection.end) {
98
+ const baseOffset = selection.start - lineRange.start;
99
+ const nextOffset = Math.max(0, Math.min(nextContent.length, baseOffset + (nextContent.length - lineRange.content.length)));
100
+ const cursor = lineRange.start + nextOffset;
101
+ return {
102
+ text: nextText,
103
+ selection: { start: cursor, end: cursor },
104
+ activeInlineActions,
105
+ };
106
+ }
107
+ return {
108
+ text: nextText,
109
+ selection: {
110
+ start: lineRange.start,
111
+ end: lineRange.start + nextContent.length,
112
+ },
113
+ activeInlineActions,
114
+ };
115
+ }
116
+ /** Dispatches per-action line transformations. */
117
+ function transformLinesByAction(action, lines) {
118
+ switch (action) {
119
+ case 'heading':
120
+ return togglePrefix(lines, /^#{1,6}\s+/, () => '# ');
121
+ case 'heading1':
122
+ return setHeadingLevel(lines, 1);
123
+ case 'heading2':
124
+ return setHeadingLevel(lines, 2);
125
+ case 'heading3':
126
+ return setHeadingLevel(lines, 3);
127
+ case 'heading4':
128
+ return setHeadingLevel(lines, 4);
129
+ case 'heading5':
130
+ return setHeadingLevel(lines, 5);
131
+ case 'heading6':
132
+ return setHeadingLevel(lines, 6);
133
+ case 'quote':
134
+ return togglePrefix(lines, /^>\s?/, () => '> ');
135
+ case 'unorderedList':
136
+ return togglePrefix(lines, /^[-*+]\s+/, () => '- ');
137
+ case 'orderedList':
138
+ return togglePrefix(lines, /^\d+\.\s+/, (index) => `${index + 1}. `);
139
+ case 'divider':
140
+ return lines.every((line) => /^ {0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line))
141
+ ? lines.map(() => '')
142
+ : lines.map(() => '---');
143
+ case 'codeBlock':
144
+ return toggleCodeBlock(lines);
145
+ default:
146
+ return lines;
147
+ }
148
+ }
149
+ /** Sets (or removes) a specific heading level on the given lines. */
150
+ function setHeadingLevel(lines, level) {
151
+ const prefix = `${'#'.repeat(level)} `;
152
+ const stripHeading = /^#{1,6}\s+/;
153
+ const hasSameLevel = lines.every((line) => new RegExp(`^#{${level}}\\s+`).test(line));
154
+ if (hasSameLevel) {
155
+ return lines.map((line) => line.replace(stripHeading, ''));
156
+ }
157
+ return lines.map((line) => `${prefix}${line.replace(stripHeading, '')}`);
158
+ }
159
+ /** Toggles a line prefix (e.g. `> `, `- `, `1. `) on every line. */
160
+ function togglePrefix(lines, stripPattern, prefixFactory) {
161
+ const hasPrefix = lines.every((line) => stripPattern.test(line));
162
+ if (hasPrefix) {
163
+ return lines.map((line) => line.replace(stripPattern, ''));
164
+ }
165
+ return lines.map((line, index) => `${prefixFactory(index)}${line}`);
166
+ }
167
+ /** Wraps or unwraps lines in a fenced code block (` ``` `). */
168
+ function toggleCodeBlock(lines) {
169
+ const hasCodeBlockMarker = (line) => /^```/.test(line);
170
+ const isWrappedInCodeBlock = lines.length >= 2 &&
171
+ hasCodeBlockMarker(lines[0]) &&
172
+ hasCodeBlockMarker(lines[lines.length - 1]);
173
+ if (isWrappedInCodeBlock) {
174
+ return lines.slice(1, -1);
175
+ }
176
+ return ['```', ...lines, '```'];
177
+ }
178
+ /**
179
+ * Expands the selection to cover complete lines and returns the
180
+ * start/end offsets together with the extracted content.
181
+ */
182
+ function getSelectedLineRange(text, selection) {
183
+ const safeStart = Math.max(0, Math.min(selection.start, text.length));
184
+ const safeEnd = Math.max(0, Math.min(selection.end, text.length));
185
+ const start = text.lastIndexOf('\n', Math.max(safeStart - 1, 0));
186
+ const end = text.indexOf('\n', safeEnd);
187
+ const rangeStart = start === -1 ? 0 : start + 1;
188
+ const rangeEnd = end === -1 ? text.length : end;
189
+ return {
190
+ start: rangeStart,
191
+ end: rangeEnd,
192
+ content: text.slice(rangeStart, rangeEnd),
193
+ };
194
+ }
195
+ /** Type guard: returns `true` for inline formatting actions. */
196
+ function isInlineToolbarAction(action) {
197
+ return action === 'bold' || action === 'italic' || action === 'strikethrough' || action === 'code';
198
+ }
199
+ /** Inserts an empty image markdown template at the current cursor position. */
200
+ function applyImageAction({ text, selection, activeInlineActions, }) {
201
+ const template = '![](url)';
202
+ const cursor = selection.start;
203
+ const nextText = `${text.slice(0, cursor)}${template}${text.slice(cursor)}`;
204
+ // Place cursor inside the alt text brackets: ![|](url)
205
+ const nextCursor = cursor + 2;
206
+ return {
207
+ text: nextText,
208
+ selection: { start: nextCursor, end: nextCursor },
209
+ activeInlineActions,
210
+ };
211
+ }
212
+ //# sourceMappingURL=markdownToolbarActions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdownToolbarActions.js","sourceRoot":"","sources":["../src/markdownToolbarActions.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n MarkdownInlineToolbarAction,\n MarkdownSelection,\n MarkdownToolbarAction,\n MarkdownToolbarActionResult,\n} from './markdownCore.types';\n\nexport const DEFAULT_MARKDOWN_FEATURES: MarkdownToolbarAction[] = [\n 'bold',\n 'italic',\n 'strikethrough',\n 'code',\n 'codeBlock',\n 'heading1',\n 'heading2',\n 'heading3',\n 'heading4',\n 'heading5',\n 'heading6',\n 'quote',\n 'unorderedList',\n 'orderedList',\n 'divider',\n 'image',\n];\n\nconst INLINE_ACTION_MARKERS: Record<MarkdownInlineToolbarAction, { open: string; close: string }> = {\n bold: { open: '**', close: '**' },\n italic: { open: '_', close: '_' },\n strikethrough: { open: '~~', close: '~~' },\n code: { open: '`', close: '`' },\n};\n\n/** Parameters accepted by {@link applyMarkdownToolbarAction}. */\ntype ApplyMarkdownToolbarActionParams = {\n action: MarkdownToolbarAction;\n text: string;\n selection: MarkdownSelection;\n activeInlineActions?: MarkdownInlineToolbarAction[];\n};\n\nexport function applyMarkdownToolbarAction({\n action,\n text,\n selection,\n activeInlineActions = [],\n}: ApplyMarkdownToolbarActionParams): MarkdownToolbarActionResult {\n if (action === 'image') {\n return applyImageAction({ text, selection, activeInlineActions });\n }\n\n if (isInlineToolbarAction(action)) {\n return applyInlineAction({\n action,\n text,\n selection,\n activeInlineActions,\n });\n }\n\n return applyBlockAction({\n action,\n text,\n selection,\n activeInlineActions,\n });\n}\n\n/**\n * Wraps or unwraps the selected text (or inserts markers at cursor)\n * for an inline formatting action (bold, italic, strikethrough, code).\n */\nfunction applyInlineAction({\n action,\n text,\n selection,\n activeInlineActions,\n}: {\n action: MarkdownInlineToolbarAction;\n text: string;\n selection: MarkdownSelection;\n activeInlineActions: MarkdownInlineToolbarAction[];\n}): MarkdownToolbarActionResult {\n const marker = INLINE_ACTION_MARKERS[action];\n if (!marker) {\n return {\n text,\n selection,\n activeInlineActions,\n };\n }\n\n if (selection.start !== selection.end) {\n const selectedText = text.slice(selection.start, selection.end);\n const nextText = `${text.slice(0, selection.start)}${marker.open}${selectedText}${marker.close}${text.slice(selection.end)}`;\n const markerSize = marker.open.length;\n\n return {\n text: nextText,\n selection: {\n start: selection.start + markerSize,\n end: selection.end + markerSize,\n },\n activeInlineActions: activeInlineActions.filter((value) => value !== action),\n };\n }\n\n const cursor = selection.start;\n const isActive = activeInlineActions.includes(action);\n const nextMarker = isActive ? marker.close : marker.open;\n const nextText = `${text.slice(0, cursor)}${nextMarker}${text.slice(cursor)}`;\n const nextSelection = {\n start: cursor + nextMarker.length,\n end: cursor + nextMarker.length,\n };\n const nextActiveInlineActions = isActive\n ? activeInlineActions.filter((value) => value !== action)\n : [...activeInlineActions, action];\n\n return {\n text: nextText,\n selection: nextSelection,\n activeInlineActions: nextActiveInlineActions,\n };\n}\n\n/**\n * Applies a block-level toolbar action (heading, quote, list, divider, code block).\n * Identifies the lines covered by the current selection and transforms them.\n */\nfunction applyBlockAction({\n action,\n text,\n selection,\n activeInlineActions,\n}: {\n action: Exclude<MarkdownToolbarAction, MarkdownInlineToolbarAction>;\n text: string;\n selection: MarkdownSelection;\n activeInlineActions: MarkdownInlineToolbarAction[];\n}): MarkdownToolbarActionResult {\n const lineRange = getSelectedLineRange(text, selection);\n const lines = lineRange.content.split('\\n');\n const nextLines = transformLinesByAction(action, lines);\n const nextContent = nextLines.join('\\n');\n const nextText = `${text.slice(0, lineRange.start)}${nextContent}${text.slice(lineRange.end)}`;\n\n if (selection.start === selection.end) {\n const baseOffset = selection.start - lineRange.start;\n const nextOffset = Math.max(0, Math.min(nextContent.length, baseOffset + (nextContent.length - lineRange.content.length)));\n const cursor = lineRange.start + nextOffset;\n return {\n text: nextText,\n selection: { start: cursor, end: cursor },\n activeInlineActions,\n };\n }\n\n return {\n text: nextText,\n selection: {\n start: lineRange.start,\n end: lineRange.start + nextContent.length,\n },\n activeInlineActions,\n };\n}\n\n/** Dispatches per-action line transformations. */\nfunction transformLinesByAction(\n action: Exclude<MarkdownToolbarAction, MarkdownInlineToolbarAction>,\n lines: string[]\n): string[] {\n switch (action) {\n case 'heading':\n return togglePrefix(lines, /^#{1,6}\\s+/, () => '# ');\n case 'heading1':\n return setHeadingLevel(lines, 1);\n case 'heading2':\n return setHeadingLevel(lines, 2);\n case 'heading3':\n return setHeadingLevel(lines, 3);\n case 'heading4':\n return setHeadingLevel(lines, 4);\n case 'heading5':\n return setHeadingLevel(lines, 5);\n case 'heading6':\n return setHeadingLevel(lines, 6);\n case 'quote':\n return togglePrefix(lines, /^>\\s?/, () => '> ');\n case 'unorderedList':\n return togglePrefix(lines, /^[-*+]\\s+/, () => '- ');\n case 'orderedList':\n return togglePrefix(lines, /^\\d+\\.\\s+/, (index) => `${index + 1}. `);\n case 'divider':\n return lines.every((line) => /^ {0,3}([-*_])[ \\t]*(?:\\1[ \\t]*){2,}$/.test(line))\n ? lines.map(() => '')\n : lines.map(() => '---');\n case 'codeBlock':\n return toggleCodeBlock(lines);\n default:\n return lines;\n }\n}\n\n/** Sets (or removes) a specific heading level on the given lines. */\nfunction setHeadingLevel(lines: string[], level: 1 | 2 | 3 | 4 | 5 | 6): string[] {\n const prefix = `${'#'.repeat(level)} `;\n const stripHeading = /^#{1,6}\\s+/;\n const hasSameLevel = lines.every((line) => new RegExp(`^#{${level}}\\\\s+`).test(line));\n if (hasSameLevel) {\n return lines.map((line) => line.replace(stripHeading, ''));\n }\n return lines.map((line) => `${prefix}${line.replace(stripHeading, '')}`);\n}\n\n/** Toggles a line prefix (e.g. `> `, `- `, `1. `) on every line. */\nfunction togglePrefix(\n lines: string[],\n stripPattern: RegExp,\n prefixFactory: (index: number) => string\n): string[] {\n const hasPrefix = lines.every((line) => stripPattern.test(line));\n\n if (hasPrefix) {\n return lines.map((line) => line.replace(stripPattern, ''));\n }\n\n return lines.map((line, index) => `${prefixFactory(index)}${line}`);\n}\n\n/** Wraps or unwraps lines in a fenced code block (` ``` `). */\nfunction toggleCodeBlock(lines: string[]): string[] {\n const hasCodeBlockMarker = (line: string) => /^```/.test(line);\n const isWrappedInCodeBlock =\n lines.length >= 2 &&\n hasCodeBlockMarker(lines[0]) &&\n hasCodeBlockMarker(lines[lines.length - 1]);\n\n if (isWrappedInCodeBlock) {\n return lines.slice(1, -1);\n }\n\n return ['```', ...lines, '```'];\n}\n\n/**\n * Expands the selection to cover complete lines and returns the\n * start/end offsets together with the extracted content.\n */\nfunction getSelectedLineRange(text: string, selection: MarkdownSelection): {\n start: number;\n end: number;\n content: string;\n} {\n const safeStart = Math.max(0, Math.min(selection.start, text.length));\n const safeEnd = Math.max(0, Math.min(selection.end, text.length));\n const start = text.lastIndexOf('\\n', Math.max(safeStart - 1, 0));\n const end = text.indexOf('\\n', safeEnd);\n\n const rangeStart = start === -1 ? 0 : start + 1;\n const rangeEnd = end === -1 ? text.length : end;\n\n return {\n start: rangeStart,\n end: rangeEnd,\n content: text.slice(rangeStart, rangeEnd),\n };\n}\n\n/** Type guard: returns `true` for inline formatting actions. */\nfunction isInlineToolbarAction(action: MarkdownToolbarAction): action is MarkdownInlineToolbarAction {\n return action === 'bold' || action === 'italic' || action === 'strikethrough' || action === 'code';\n}\n\n/** Inserts an empty image markdown template at the current cursor position. */\nfunction applyImageAction({\n text,\n selection,\n activeInlineActions,\n}: {\n text: string;\n selection: MarkdownSelection;\n activeInlineActions: MarkdownInlineToolbarAction[];\n}): MarkdownToolbarActionResult {\n const template = '![](url)';\n const cursor = selection.start;\n const nextText = `${text.slice(0, cursor)}${template}${text.slice(cursor)}`;\n // Place cursor inside the alt text brackets: ![|](url)\n const nextCursor = cursor + 2;\n return {\n text: nextText,\n selection: { start: nextCursor, end: nextCursor },\n activeInlineActions,\n };\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import type { MarkdownEditorHandle, UseMarkdownEditorOptions } from './markdownCore.types';
2
+ /**
3
+ * Hook that manages the shared state between MarkdownTextInput and MarkdownToolbar.
4
+ *
5
+ * Returns a `MarkdownEditorHandle` object to pass as `editor` prop to both components.
6
+ * Handles selection tracking, active inline actions, toolbar action application,
7
+ * and syntax highlighting.
8
+ */
9
+ export declare function useMarkdownEditor({ value, onChangeText, onSelectionChange, onToolbarAction, features }: UseMarkdownEditorOptions): MarkdownEditorHandle;
10
+ //# sourceMappingURL=useMarkdownEditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMarkdownEditor.d.ts","sourceRoot":"","sources":["../src/useMarkdownEditor.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,oBAAoB,EAKpB,wBAAwB,EACxB,MAAM,sBAAsB,CAAC;AAW9B;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,QAAQ,EAAE,EAAE,wBAAwB,GAAG,oBAAoB,CA4OvJ"}
@@ -0,0 +1,219 @@
1
+ import * as React from 'react';
2
+ import { Platform } from 'react-native';
3
+ import { highlightMarkdown } from './markdownHighlight';
4
+ import { applyMarkdownToolbarAction, DEFAULT_MARKDOWN_FEATURES } from './markdownToolbarActions';
5
+ // Default selection state (cursor at position 0)
6
+ const DEFAULT_SELECTION = { start: 0, end: 0 };
7
+ // ---------------------------------------------------------------------------
8
+ // Hook
9
+ // ---------------------------------------------------------------------------
10
+ /**
11
+ * Hook that manages the shared state between MarkdownTextInput and MarkdownToolbar.
12
+ *
13
+ * Returns a `MarkdownEditorHandle` object to pass as `editor` prop to both components.
14
+ * Handles selection tracking, active inline actions, toolbar action application,
15
+ * and syntax highlighting.
16
+ */
17
+ export function useMarkdownEditor({ value, onChangeText, onSelectionChange, onToolbarAction, features }) {
18
+ // Reference to the TextInput component for programmatic focus and selection
19
+ const inputRef = React.useRef(null);
20
+ // Resolve features with default fallback
21
+ const resolvedFeatures = features ?? DEFAULT_MARKDOWN_FEATURES;
22
+ // Ref to track the current selection (synchronous access needed for event handlers)
23
+ const selectionRef = React.useRef(DEFAULT_SELECTION);
24
+ // State for current text selection (triggers re-renders when updated)
25
+ const [selection, setSelection] = React.useState(DEFAULT_SELECTION);
26
+ // Track which inline toolbar actions are currently active (e.g., bold, italic)
27
+ const [activeInlineActions, setActiveInlineActions] = React.useState([]);
28
+ // Flag to suppress selection change events (used during Android toolbar actions)
29
+ const suppressSelectionRef = React.useRef(false);
30
+ // Keep a ref to the current value for synchronous access in callbacks
31
+ const valueRef = React.useRef(value);
32
+ valueRef.current = value;
33
+ /**
34
+ * Android workaround: forces cursor position after programmatic text changes.
35
+ * Android fires spurious native onSelectionChange events that override the
36
+ * desired cursor position - this suppresses them and applies the selection
37
+ * via setNativeProps across sequential animation frames.
38
+ */
39
+ const applyAndroidSelection = React.useCallback((targetSelection) => {
40
+ if (Platform.OS !== 'android' || !inputRef.current)
41
+ return;
42
+ suppressSelectionRef.current = true;
43
+ requestAnimationFrame(() => {
44
+ inputRef.current?.focus();
45
+ requestAnimationFrame(() => {
46
+ if (inputRef.current) {
47
+ inputRef.current.setNativeProps({
48
+ selection: targetSelection,
49
+ });
50
+ }
51
+ requestAnimationFrame(() => {
52
+ suppressSelectionRef.current = false;
53
+ });
54
+ });
55
+ });
56
+ }, []);
57
+ // Parse and highlight the markdown text for syntax highlighting
58
+ const highlightedSegments = React.useMemo(() => highlightMarkdown(value, resolvedFeatures), [value, resolvedFeatures]);
59
+ /**
60
+ * Detects if the cursor is currently positioned within an image markdown syntax.
61
+ * Returns image metadata if found, null otherwise.
62
+ */
63
+ const activeImageInfo = React.useMemo(() => {
64
+ const selStart = selection.start;
65
+ const selEnd = selection.end;
66
+ // Search through highlighted segments to find if cursor/selection overlaps an image
67
+ let offset = 0;
68
+ for (const seg of highlightedSegments) {
69
+ const segEnd = offset + seg.text.length;
70
+ if (selStart < segEnd && selEnd > offset && seg.type === 'image' && seg.meta?.src) {
71
+ return {
72
+ src: seg.meta.src,
73
+ alt: seg.meta.alt ?? '',
74
+ title: seg.meta.title,
75
+ start: offset,
76
+ end: segEnd,
77
+ };
78
+ }
79
+ offset = segEnd;
80
+ }
81
+ return null;
82
+ }, [selection, highlightedSegments]);
83
+ /**
84
+ * Handles text changes in the editor.
85
+ */
86
+ const handleChangeText = React.useCallback((nextValue) => {
87
+ if (nextValue === valueRef.current)
88
+ return;
89
+ onChangeText(nextValue);
90
+ }, [onChangeText]);
91
+ /**
92
+ * Handles selection changes in the text input.
93
+ * Suppressed during Android toolbar actions to prevent spurious events.
94
+ */
95
+ const handleSelectionChange = React.useCallback((event) => {
96
+ // Ignore selection changes when suppression flag is set (Android toolbar actions)
97
+ if (suppressSelectionRef.current)
98
+ return;
99
+ const nextSelection = event.nativeEvent.selection;
100
+ selectionRef.current = nextSelection;
101
+ setSelection(nextSelection);
102
+ onSelectionChange?.(nextSelection);
103
+ }, [onSelectionChange]);
104
+ /**
105
+ * Handles toolbar button actions (bold, italic, heading, etc.).
106
+ * Applies the markdown transformation and manages focus/selection.
107
+ */
108
+ const handleToolbarAction = React.useCallback((action) => {
109
+ const currentSelection = selectionRef.current;
110
+ // Apply the markdown transformation based on the action
111
+ const result = applyMarkdownToolbarAction({
112
+ action,
113
+ text: value,
114
+ selection: currentSelection,
115
+ activeInlineActions,
116
+ });
117
+ // Update state with the transformation result
118
+ selectionRef.current = result.selection;
119
+ setSelection(result.selection);
120
+ setActiveInlineActions(result.activeInlineActions);
121
+ onChangeText(result.text);
122
+ onSelectionChange?.(result.selection);
123
+ onToolbarAction?.(action, result);
124
+ // Android-specific workaround for selection handling
125
+ if (Platform.OS === 'android') {
126
+ applyAndroidSelection(result.selection);
127
+ }
128
+ else {
129
+ // iOS: Simply refocus the input
130
+ inputRef.current?.focus();
131
+ }
132
+ }, [activeInlineActions, onChangeText, onSelectionChange, onToolbarAction, value, applyAndroidSelection]);
133
+ // State for the image info modal/popup
134
+ const [imageInfo, setImageInfo] = React.useState(null);
135
+ /**
136
+ * Opens the image info popup for the currently active image.
137
+ */
138
+ const openImageInfo = React.useCallback(() => {
139
+ if (activeImageInfo)
140
+ setImageInfo(activeImageInfo);
141
+ }, [activeImageInfo]);
142
+ /**
143
+ * Closes the image info popup.
144
+ */
145
+ const dismissImageInfo = React.useCallback(() => {
146
+ setImageInfo(null);
147
+ }, []);
148
+ /**
149
+ * Deletes the currently active image from the markdown text.
150
+ * Also removes the trailing newline if present.
151
+ */
152
+ const deleteActiveImage = React.useCallback(() => {
153
+ if (!activeImageInfo)
154
+ return;
155
+ const before = value.slice(0, activeImageInfo.start);
156
+ const after = value.slice(activeImageInfo.end);
157
+ // Remove trailing newline if the image is followed by one
158
+ const nextValue = after.startsWith('\n') ? before + after.slice(1) : before + after;
159
+ // Position cursor at the start of where the image was
160
+ const cursor = activeImageInfo.start;
161
+ selectionRef.current = { start: cursor, end: cursor };
162
+ setSelection({ start: cursor, end: cursor });
163
+ setImageInfo(null);
164
+ onChangeText(nextValue);
165
+ }, [activeImageInfo, value, onChangeText]);
166
+ /**
167
+ * Effect: Clamp selection to valid range when text value shrinks.
168
+ * Prevents selection indices from being out of bounds.
169
+ */
170
+ React.useEffect(() => {
171
+ const currentSelection = selectionRef.current;
172
+ // Check if current selection is still valid
173
+ if (currentSelection.start <= value.length && currentSelection.end <= value.length) {
174
+ return;
175
+ }
176
+ // Clamp selection to the maximum valid position
177
+ const safeSelection = {
178
+ start: Math.min(currentSelection.start, value.length),
179
+ end: Math.min(currentSelection.end, value.length),
180
+ };
181
+ selectionRef.current = safeSelection;
182
+ setSelection(safeSelection);
183
+ }, [value.length]);
184
+ /**
185
+ * Return the editor handle object.
186
+ * Memoized to maintain stable reference across renders.
187
+ */
188
+ return React.useMemo(() => ({
189
+ features: resolvedFeatures,
190
+ value,
191
+ selection,
192
+ activeInlineActions,
193
+ activeImageInfo,
194
+ imageInfo,
195
+ openImageInfo,
196
+ dismissImageInfo,
197
+ highlightedSegments,
198
+ inputRef,
199
+ handleChangeText,
200
+ handleSelectionChange,
201
+ handleToolbarAction,
202
+ deleteActiveImage,
203
+ }), [
204
+ resolvedFeatures,
205
+ value,
206
+ selection,
207
+ activeInlineActions,
208
+ activeImageInfo,
209
+ imageInfo,
210
+ openImageInfo,
211
+ dismissImageInfo,
212
+ highlightedSegments,
213
+ handleChangeText,
214
+ handleSelectionChange,
215
+ handleToolbarAction,
216
+ deleteActiveImage,
217
+ ]);
218
+ }
219
+ //# sourceMappingURL=useMarkdownEditor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMarkdownEditor.js","sourceRoot":"","sources":["../src/useMarkdownEditor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,QAAQ,EAAa,MAAM,cAAc,CAAC;AAUnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AAEjG,iDAAiD;AACjD,MAAM,iBAAiB,GAAsB,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;AAElE,8EAA8E;AAC9E,OAAO;AACP,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,QAAQ,EAA4B;IAChI,4EAA4E;IAC5E,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAY,IAAI,CAAC,CAAC;IAE/C,yCAAyC;IACzC,MAAM,gBAAgB,GAAG,QAAQ,IAAI,yBAAyB,CAAC;IAE/D,oFAAoF;IACpF,MAAM,YAAY,GAAG,KAAK,CAAC,MAAM,CAAoB,iBAAiB,CAAC,CAAC;IAExE,sEAAsE;IACtE,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAoB,iBAAiB,CAAC,CAAC;IAEvF,+EAA+E;IAC/E,MAAM,CAAC,mBAAmB,EAAE,sBAAsB,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAgC,EAAE,CAAC,CAAC;IAExG,iFAAiF;IACjF,MAAM,oBAAoB,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAEjD,sEAAsE;IACtE,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACrC,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAC;IAEzB;;;;;OAKG;IACH,MAAM,qBAAqB,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC,eAAkC,EAAE,EAAE;QACtF,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,IAAI,CAAC,QAAQ,CAAC,OAAO;YAAE,OAAO;QAE3D,oBAAoB,CAAC,OAAO,GAAG,IAAI,CAAC;QACpC,qBAAqB,CAAC,GAAG,EAAE;YAC1B,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;YAC1B,qBAAqB,CAAC,GAAG,EAAE;gBAC1B,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;oBACtB,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC;wBAC/B,SAAS,EAAE,eAAe;qBAC1B,CAAC,CAAC;gBACJ,CAAC;gBACD,qBAAqB,CAAC,GAAG,EAAE;oBAC1B,oBAAoB,CAAC,OAAO,GAAG,KAAK,CAAC;gBACtC,CAAC,CAAC,CAAC;YACJ,CAAC,CAAC,CAAC;QACJ,CAAC,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,gEAAgE;IAChE,MAAM,mBAAmB,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAEvH;;;OAGG;IACH,MAAM,eAAe,GAAG,KAAK,CAAC,OAAO,CAA2B,GAAG,EAAE;QACpE,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAC;QACjC,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC;QAE7B,oFAAoF;QACpF,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,KAAK,MAAM,GAAG,IAAI,mBAAmB,EAAE,CAAC;YACvC,MAAM,MAAM,GAAG,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC;YACxC,IAAI,QAAQ,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,IAAI,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC;gBACnF,OAAO;oBACN,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,GAAG;oBACjB,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE;oBACvB,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK;oBACrB,KAAK,EAAE,MAAM;oBACb,GAAG,EAAE,MAAM;iBACX,CAAC;YACH,CAAC;YACD,MAAM,GAAG,MAAM,CAAC;QACjB,CAAC;QACD,OAAO,IAAI,CAAC;IACb,CAAC,EAAE,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC,CAAC;IAErC;;OAEG;IACH,MAAM,gBAAgB,GAAG,KAAK,CAAC,WAAW,CACzC,CAAC,SAAiB,EAAE,EAAE;QACrB,IAAI,SAAS,KAAK,QAAQ,CAAC,OAAO;YAAE,OAAO;QAC3C,YAAY,CAAC,SAAS,CAAC,CAAC;IACzB,CAAC,EACD,CAAC,YAAY,CAAC,CACd,CAAC;IAEF;;;OAGG;IACH,MAAM,qBAAqB,GAAG,KAAK,CAAC,WAAW,CAC9C,CAAC,KAAwD,EAAE,EAAE;QAC5D,kFAAkF;QAClF,IAAI,oBAAoB,CAAC,OAAO;YAAE,OAAO;QAEzC,MAAM,aAAa,GAAG,KAAK,CAAC,WAAW,CAAC,SAAS,CAAC;QAClD,YAAY,CAAC,OAAO,GAAG,aAAa,CAAC;QACrC,YAAY,CAAC,aAAa,CAAC,CAAC;QAC5B,iBAAiB,EAAE,CAAC,aAAa,CAAC,CAAC;IACpC,CAAC,EACD,CAAC,iBAAiB,CAAC,CACnB,CAAC;IAEF;;;OAGG;IACH,MAAM,mBAAmB,GAAG,KAAK,CAAC,WAAW,CAC5C,CAAC,MAA6B,EAAE,EAAE;QACjC,MAAM,gBAAgB,GAAG,YAAY,CAAC,OAAO,CAAC;QAE9C,wDAAwD;QACxD,MAAM,MAAM,GAAG,0BAA0B,CAAC;YACzC,MAAM;YACN,IAAI,EAAE,KAAK;YACX,SAAS,EAAE,gBAAgB;YAC3B,mBAAmB;SACnB,CAAC,CAAC;QAEH,8CAA8C;QAC9C,YAAY,CAAC,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC;QACxC,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC/B,sBAAsB,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;QACnD,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1B,iBAAiB,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACtC,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAElC,qDAAqD;QACrD,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;YAC/B,qBAAqB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACzC,CAAC;aAAM,CAAC;YACP,gCAAgC;YAChC,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;QAC3B,CAAC;IACF,CAAC,EACD,CAAC,mBAAmB,EAAE,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,KAAK,EAAE,qBAAqB,CAAC,CACrG,CAAC;IAEF,uCAAuC;IACvC,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAA2B,IAAI,CAAC,CAAC;IAEjF;;OAEG;IACH,MAAM,aAAa,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE;QAC5C,IAAI,eAAe;YAAE,YAAY,CAAC,eAAe,CAAC,CAAC;IACpD,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IAEtB;;OAEG;IACH,MAAM,gBAAgB,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE;QAC/C,YAAY,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP;;;OAGG;IACH,MAAM,iBAAiB,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE;QAChD,IAAI,CAAC,eAAe;YAAE,OAAO;QAE7B,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC;QACrD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QAE/C,0DAA0D;QAC1D,MAAM,SAAS,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC;QAEpF,sDAAsD;QACtD,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC;QACrC,YAAY,CAAC,OAAO,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;QACtD,YAAY,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;QAC7C,YAAY,CAAC,IAAI,CAAC,CAAC;QACnB,YAAY,CAAC,SAAS,CAAC,CAAC;IACzB,CAAC,EAAE,CAAC,eAAe,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC;IAE3C;;;OAGG;IACH,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACpB,MAAM,gBAAgB,GAAG,YAAY,CAAC,OAAO,CAAC;QAE9C,4CAA4C;QAC5C,IAAI,gBAAgB,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,IAAI,gBAAgB,CAAC,GAAG,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YACpF,OAAO;QACR,CAAC;QAED,gDAAgD;QAChD,MAAM,aAAa,GAAG;YACrB,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC;YACrD,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC;SACjD,CAAC;QACF,YAAY,CAAC,OAAO,GAAG,aAAa,CAAC;QACrC,YAAY,CAAC,aAAa,CAAC,CAAC;IAC7B,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IAEnB;;;OAGG;IACH,OAAO,KAAK,CAAC,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC;QACN,QAAQ,EAAE,gBAAgB;QAC1B,KAAK;QACL,SAAS;QACT,mBAAmB;QACnB,eAAe;QACf,SAAS;QACT,aAAa;QACb,gBAAgB;QAChB,mBAAmB;QACnB,QAAQ;QACR,gBAAgB;QAChB,qBAAqB;QACrB,mBAAmB;QACnB,iBAAiB;KACjB,CAAC,EACF;QACC,gBAAgB;QAChB,KAAK;QACL,SAAS;QACT,mBAAmB;QACnB,eAAe;QACf,SAAS;QACT,aAAa;QACb,gBAAgB;QAChB,mBAAmB;QACnB,gBAAgB;QAChB,qBAAqB;QACrB,mBAAmB;QACnB,iBAAiB;KACjB,CACD,CAAC;AACH,CAAC","sourcesContent":["import * as React from 'react';\nimport { Platform, TextInput } from 'react-native';\n\nimport type {\n\tMarkdownEditorHandle,\n\tMarkdownImageInfo,\n\tMarkdownInlineToolbarAction,\n\tMarkdownSelection,\n\tMarkdownToolbarAction,\n\tUseMarkdownEditorOptions,\n} from './markdownCore.types';\nimport { highlightMarkdown } from './markdownHighlight';\nimport { applyMarkdownToolbarAction, DEFAULT_MARKDOWN_FEATURES } from './markdownToolbarActions';\n\n// Default selection state (cursor at position 0)\nconst DEFAULT_SELECTION: MarkdownSelection = { start: 0, end: 0 };\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\n/**\n * Hook that manages the shared state between MarkdownTextInput and MarkdownToolbar.\n *\n * Returns a `MarkdownEditorHandle` object to pass as `editor` prop to both components.\n * Handles selection tracking, active inline actions, toolbar action application,\n * and syntax highlighting.\n */\nexport function useMarkdownEditor({ value, onChangeText, onSelectionChange, onToolbarAction, features }: UseMarkdownEditorOptions): MarkdownEditorHandle {\n\t// Reference to the TextInput component for programmatic focus and selection\n\tconst inputRef = React.useRef<TextInput>(null);\n\n\t// Resolve features with default fallback\n\tconst resolvedFeatures = features ?? DEFAULT_MARKDOWN_FEATURES;\n\n\t// Ref to track the current selection (synchronous access needed for event handlers)\n\tconst selectionRef = React.useRef<MarkdownSelection>(DEFAULT_SELECTION);\n\n\t// State for current text selection (triggers re-renders when updated)\n\tconst [selection, setSelection] = React.useState<MarkdownSelection>(DEFAULT_SELECTION);\n\n\t// Track which inline toolbar actions are currently active (e.g., bold, italic)\n\tconst [activeInlineActions, setActiveInlineActions] = React.useState<MarkdownInlineToolbarAction[]>([]);\n\n\t// Flag to suppress selection change events (used during Android toolbar actions)\n\tconst suppressSelectionRef = React.useRef(false);\n\n\t// Keep a ref to the current value for synchronous access in callbacks\n\tconst valueRef = React.useRef(value);\n\tvalueRef.current = value;\n\n\t/**\n\t * Android workaround: forces cursor position after programmatic text changes.\n\t * Android fires spurious native onSelectionChange events that override the\n\t * desired cursor position - this suppresses them and applies the selection\n\t * via setNativeProps across sequential animation frames.\n\t */\n\tconst applyAndroidSelection = React.useCallback((targetSelection: MarkdownSelection) => {\n\t\tif (Platform.OS !== 'android' || !inputRef.current) return;\n\n\t\tsuppressSelectionRef.current = true;\n\t\trequestAnimationFrame(() => {\n\t\t\tinputRef.current?.focus();\n\t\t\trequestAnimationFrame(() => {\n\t\t\t\tif (inputRef.current) {\n\t\t\t\t\tinputRef.current.setNativeProps({\n\t\t\t\t\t\tselection: targetSelection,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\trequestAnimationFrame(() => {\n\t\t\t\t\tsuppressSelectionRef.current = false;\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\t}, []);\n\n\t// Parse and highlight the markdown text for syntax highlighting\n\tconst highlightedSegments = React.useMemo(() => highlightMarkdown(value, resolvedFeatures), [value, resolvedFeatures]);\n\n\t/**\n\t * Detects if the cursor is currently positioned within an image markdown syntax.\n\t * Returns image metadata if found, null otherwise.\n\t */\n\tconst activeImageInfo = React.useMemo<MarkdownImageInfo | null>(() => {\n\t\tconst selStart = selection.start;\n\t\tconst selEnd = selection.end;\n\n\t\t// Search through highlighted segments to find if cursor/selection overlaps an image\n\t\tlet offset = 0;\n\t\tfor (const seg of highlightedSegments) {\n\t\t\tconst segEnd = offset + seg.text.length;\n\t\t\tif (selStart < segEnd && selEnd > offset && seg.type === 'image' && seg.meta?.src) {\n\t\t\t\treturn {\n\t\t\t\t\tsrc: seg.meta.src,\n\t\t\t\t\talt: seg.meta.alt ?? '',\n\t\t\t\t\ttitle: seg.meta.title,\n\t\t\t\t\tstart: offset,\n\t\t\t\t\tend: segEnd,\n\t\t\t\t};\n\t\t\t}\n\t\t\toffset = segEnd;\n\t\t}\n\t\treturn null;\n\t}, [selection, highlightedSegments]);\n\n\t/**\n\t * Handles text changes in the editor.\n\t */\n\tconst handleChangeText = React.useCallback(\n\t\t(nextValue: string) => {\n\t\t\tif (nextValue === valueRef.current) return;\n\t\t\tonChangeText(nextValue);\n\t\t},\n\t\t[onChangeText],\n\t);\n\n\t/**\n\t * Handles selection changes in the text input.\n\t * Suppressed during Android toolbar actions to prevent spurious events.\n\t */\n\tconst handleSelectionChange = React.useCallback(\n\t\t(event: { nativeEvent: { selection: MarkdownSelection } }) => {\n\t\t\t// Ignore selection changes when suppression flag is set (Android toolbar actions)\n\t\t\tif (suppressSelectionRef.current) return;\n\n\t\t\tconst nextSelection = event.nativeEvent.selection;\n\t\t\tselectionRef.current = nextSelection;\n\t\t\tsetSelection(nextSelection);\n\t\t\tonSelectionChange?.(nextSelection);\n\t\t},\n\t\t[onSelectionChange],\n\t);\n\n\t/**\n\t * Handles toolbar button actions (bold, italic, heading, etc.).\n\t * Applies the markdown transformation and manages focus/selection.\n\t */\n\tconst handleToolbarAction = React.useCallback(\n\t\t(action: MarkdownToolbarAction) => {\n\t\t\tconst currentSelection = selectionRef.current;\n\n\t\t\t// Apply the markdown transformation based on the action\n\t\t\tconst result = applyMarkdownToolbarAction({\n\t\t\t\taction,\n\t\t\t\ttext: value,\n\t\t\t\tselection: currentSelection,\n\t\t\t\tactiveInlineActions,\n\t\t\t});\n\n\t\t\t// Update state with the transformation result\n\t\t\tselectionRef.current = result.selection;\n\t\t\tsetSelection(result.selection);\n\t\t\tsetActiveInlineActions(result.activeInlineActions);\n\t\t\tonChangeText(result.text);\n\t\t\tonSelectionChange?.(result.selection);\n\t\t\tonToolbarAction?.(action, result);\n\n\t\t\t// Android-specific workaround for selection handling\n\t\t\tif (Platform.OS === 'android') {\n\t\t\t\tapplyAndroidSelection(result.selection);\n\t\t\t} else {\n\t\t\t\t// iOS: Simply refocus the input\n\t\t\t\tinputRef.current?.focus();\n\t\t\t}\n\t\t},\n\t\t[activeInlineActions, onChangeText, onSelectionChange, onToolbarAction, value, applyAndroidSelection],\n\t);\n\n\t// State for the image info modal/popup\n\tconst [imageInfo, setImageInfo] = React.useState<MarkdownImageInfo | null>(null);\n\n\t/**\n\t * Opens the image info popup for the currently active image.\n\t */\n\tconst openImageInfo = React.useCallback(() => {\n\t\tif (activeImageInfo) setImageInfo(activeImageInfo);\n\t}, [activeImageInfo]);\n\n\t/**\n\t * Closes the image info popup.\n\t */\n\tconst dismissImageInfo = React.useCallback(() => {\n\t\tsetImageInfo(null);\n\t}, []);\n\n\t/**\n\t * Deletes the currently active image from the markdown text.\n\t * Also removes the trailing newline if present.\n\t */\n\tconst deleteActiveImage = React.useCallback(() => {\n\t\tif (!activeImageInfo) return;\n\n\t\tconst before = value.slice(0, activeImageInfo.start);\n\t\tconst after = value.slice(activeImageInfo.end);\n\n\t\t// Remove trailing newline if the image is followed by one\n\t\tconst nextValue = after.startsWith('\\n') ? before + after.slice(1) : before + after;\n\n\t\t// Position cursor at the start of where the image was\n\t\tconst cursor = activeImageInfo.start;\n\t\tselectionRef.current = { start: cursor, end: cursor };\n\t\tsetSelection({ start: cursor, end: cursor });\n\t\tsetImageInfo(null);\n\t\tonChangeText(nextValue);\n\t}, [activeImageInfo, value, onChangeText]);\n\n\t/**\n\t * Effect: Clamp selection to valid range when text value shrinks.\n\t * Prevents selection indices from being out of bounds.\n\t */\n\tReact.useEffect(() => {\n\t\tconst currentSelection = selectionRef.current;\n\n\t\t// Check if current selection is still valid\n\t\tif (currentSelection.start <= value.length && currentSelection.end <= value.length) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Clamp selection to the maximum valid position\n\t\tconst safeSelection = {\n\t\t\tstart: Math.min(currentSelection.start, value.length),\n\t\t\tend: Math.min(currentSelection.end, value.length),\n\t\t};\n\t\tselectionRef.current = safeSelection;\n\t\tsetSelection(safeSelection);\n\t}, [value.length]);\n\n\t/**\n\t * Return the editor handle object.\n\t * Memoized to maintain stable reference across renders.\n\t */\n\treturn React.useMemo(\n\t\t() => ({\n\t\t\tfeatures: resolvedFeatures,\n\t\t\tvalue,\n\t\t\tselection,\n\t\t\tactiveInlineActions,\n\t\t\tactiveImageInfo,\n\t\t\timageInfo,\n\t\t\topenImageInfo,\n\t\t\tdismissImageInfo,\n\t\t\thighlightedSegments,\n\t\t\tinputRef,\n\t\t\thandleChangeText,\n\t\t\thandleSelectionChange,\n\t\t\thandleToolbarAction,\n\t\t\tdeleteActiveImage,\n\t\t}),\n\t\t[\n\t\t\tresolvedFeatures,\n\t\t\tvalue,\n\t\t\tselection,\n\t\t\tactiveInlineActions,\n\t\t\tactiveImageInfo,\n\t\t\timageInfo,\n\t\t\topenImageInfo,\n\t\t\tdismissImageInfo,\n\t\t\thighlightedSegments,\n\t\t\thandleChangeText,\n\t\t\thandleSelectionChange,\n\t\t\thandleToolbarAction,\n\t\t\tdeleteActiveImage,\n\t\t],\n\t);\n}\n"]}
package/jest.config.js ADDED
@@ -0,0 +1,10 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ module.exports = {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'node',
5
+ roots: ['<rootDir>/src'],
6
+ testMatch: ['**/__tests__/**/*.test.ts'],
7
+ transform: {
8
+ '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.test.json' }],
9
+ },
10
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@domenico-esposito/react-native-markdown-editor",
3
+ "version": "0.1.1",
4
+ "description": "Markdown Editor for React Native",
5
+ "main": "build/index.js",
6
+ "source": "src/index.ts",
7
+ "types": "build/index.d.ts",
8
+ "scripts": {
9
+ "build": "expo-module build",
10
+ "clean": "expo-module clean",
11
+ "lint": "expo-module lint",
12
+ "test": "expo-module test",
13
+ "prepare": "expo-module prepare",
14
+ "prepublishOnly": "expo-module prepublishOnly",
15
+ "expo-module": "expo-module"
16
+ },
17
+ "keywords": [
18
+ "react-native",
19
+ "expo",
20
+ "react-native-markdown-editor",
21
+ "ReactNativeMarkdownEditor"
22
+ ],
23
+ "repository": "https://github.com/domenico-esposito/react-native-markdown-editor.git",
24
+ "bugs": {
25
+ "url": "https://github.com/domenico-esposito/react-native-markdown-editor/issues"
26
+ },
27
+ "author": "Domenico Esposito (Deploynk) <mail@domenicoesposito.it> (https://github.com/domenico-esposito)",
28
+ "license": "MIT",
29
+ "homepage": "https://github.com/domenico-esposito/react-native-markdown-editor#readme",
30
+ "devDependencies": {
31
+ "@types/jest": "^30.0.0",
32
+ "@types/react": "~19.1.0",
33
+ "expo": "^54.0.27",
34
+ "expo-module-scripts": "^5.0.8",
35
+ "jest": "^30.2.0",
36
+ "react-native": "0.81.5",
37
+ "ts-jest": "^29.4.6"
38
+ },
39
+ "peerDependencies": {
40
+ "expo": "*",
41
+ "react": "*",
42
+ "react-native": "*",
43
+ "react-native-safe-area-context": "*"
44
+ }
45
+ }