react-native-richify 1.0.4 → 1.0.6

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 (75) hide show
  1. package/README.md +383 -120
  2. package/lib/commonjs/components/RenderedOutput.js +168 -0
  3. package/lib/commonjs/components/RenderedOutput.js.map +1 -0
  4. package/lib/commonjs/components/RichTextInput.js +130 -15
  5. package/lib/commonjs/components/RichTextInput.js.map +1 -1
  6. package/lib/commonjs/components/Toolbar.js +41 -2
  7. package/lib/commonjs/components/Toolbar.js.map +1 -1
  8. package/lib/commonjs/constants/defaultStyles.js +51 -1
  9. package/lib/commonjs/constants/defaultStyles.js.map +1 -1
  10. package/lib/commonjs/hooks/useFormatting.js +44 -2
  11. package/lib/commonjs/hooks/useFormatting.js.map +1 -1
  12. package/lib/commonjs/hooks/useRichText.js +75 -6
  13. package/lib/commonjs/hooks/useRichText.js.map +1 -1
  14. package/lib/commonjs/utils/formatter.js +48 -9
  15. package/lib/commonjs/utils/formatter.js.map +1 -1
  16. package/lib/commonjs/utils/parser.js +1 -1
  17. package/lib/commonjs/utils/parser.js.map +1 -1
  18. package/lib/commonjs/utils/serializer.js +102 -6
  19. package/lib/commonjs/utils/serializer.js.map +1 -1
  20. package/lib/commonjs/utils/styleMapper.js +11 -0
  21. package/lib/commonjs/utils/styleMapper.js.map +1 -1
  22. package/lib/module/components/RenderedOutput.js +163 -0
  23. package/lib/module/components/RenderedOutput.js.map +1 -0
  24. package/lib/module/components/RichTextInput.js +131 -16
  25. package/lib/module/components/RichTextInput.js.map +1 -1
  26. package/lib/module/components/Toolbar.js +41 -2
  27. package/lib/module/components/Toolbar.js.map +1 -1
  28. package/lib/module/constants/defaultStyles.js +51 -1
  29. package/lib/module/constants/defaultStyles.js.map +1 -1
  30. package/lib/module/hooks/useFormatting.js +45 -3
  31. package/lib/module/hooks/useFormatting.js.map +1 -1
  32. package/lib/module/hooks/useRichText.js +75 -6
  33. package/lib/module/hooks/useRichText.js.map +1 -1
  34. package/lib/module/utils/formatter.js +46 -9
  35. package/lib/module/utils/formatter.js.map +1 -1
  36. package/lib/module/utils/parser.js +1 -1
  37. package/lib/module/utils/parser.js.map +1 -1
  38. package/lib/module/utils/serializer.js +102 -6
  39. package/lib/module/utils/serializer.js.map +1 -1
  40. package/lib/module/utils/styleMapper.js +11 -0
  41. package/lib/module/utils/styleMapper.js.map +1 -1
  42. package/lib/typescript/src/components/RenderedOutput.d.ts +9 -0
  43. package/lib/typescript/src/components/RenderedOutput.d.ts.map +1 -0
  44. package/lib/typescript/src/components/RichTextInput.d.ts.map +1 -1
  45. package/lib/typescript/src/components/Toolbar.d.ts.map +1 -1
  46. package/lib/typescript/src/constants/defaultStyles.d.ts +1 -0
  47. package/lib/typescript/src/constants/defaultStyles.d.ts.map +1 -1
  48. package/lib/typescript/src/hooks/useFormatting.d.ts +4 -1
  49. package/lib/typescript/src/hooks/useFormatting.d.ts.map +1 -1
  50. package/lib/typescript/src/hooks/useRichText.d.ts.map +1 -1
  51. package/lib/typescript/src/index.d.ts +1 -1
  52. package/lib/typescript/src/index.d.ts.map +1 -1
  53. package/lib/typescript/src/types/index.d.ts +94 -1
  54. package/lib/typescript/src/types/index.d.ts.map +1 -1
  55. package/lib/typescript/src/utils/formatter.d.ts +9 -1
  56. package/lib/typescript/src/utils/formatter.d.ts.map +1 -1
  57. package/lib/typescript/src/utils/parser.d.ts.map +1 -1
  58. package/lib/typescript/src/utils/serializer.d.ts.map +1 -1
  59. package/lib/typescript/src/utils/styleMapper.d.ts.map +1 -1
  60. package/package.json +1 -1
  61. package/src/components/RenderedOutput.tsx +231 -0
  62. package/src/components/RichTextInput.tsx +193 -19
  63. package/src/components/Toolbar.tsx +54 -2
  64. package/src/constants/defaultStyles.d.ts +2 -1
  65. package/src/constants/defaultStyles.ts +20 -0
  66. package/src/hooks/useFormatting.ts +101 -2
  67. package/src/hooks/useRichText.ts +101 -5
  68. package/src/index.d.ts +1 -1
  69. package/src/index.ts +4 -0
  70. package/src/types/index.d.ts +94 -1
  71. package/src/types/index.ts +104 -1
  72. package/src/utils/formatter.ts +60 -6
  73. package/src/utils/parser.ts +6 -1
  74. package/src/utils/serializer.ts +150 -8
  75. package/src/utils/styleMapper.ts +21 -0
@@ -1,8 +1,10 @@
1
1
  import type {
2
2
  FormatStyle,
3
3
  HeadingLevel,
4
+ ListType,
4
5
  OutputFormat,
5
6
  StyledSegment,
7
+ TextAlign,
6
8
  } from '../types';
7
9
 
8
10
  type LineFragment = Pick<StyledSegment, 'text' | 'styles'>;
@@ -15,7 +17,29 @@ export function serializeSegments(
15
17
  format: OutputFormat = 'markdown',
16
18
  ): string {
17
19
  const lines = splitSegmentsByLine(segments);
18
- return lines.map((line) => serializeLine(line, format)).join('\n');
20
+ const blocks: string[] = [];
21
+
22
+ for (let index = 0; index < lines.length; ) {
23
+ const line = lines[index];
24
+ const listType = getLineListType(line);
25
+
26
+ if (listType && listType !== 'none') {
27
+ const listLines: LineFragment[][] = [];
28
+
29
+ while (index < lines.length && getLineListType(lines[index]) === listType) {
30
+ listLines.push(lines[index]);
31
+ index++;
32
+ }
33
+
34
+ blocks.push(serializeListBlock(listLines, format, listType));
35
+ continue;
36
+ }
37
+
38
+ blocks.push(serializeBlockLine(line, format));
39
+ index++;
40
+ }
41
+
42
+ return blocks.join('\n');
19
43
  }
20
44
 
21
45
  /**
@@ -39,7 +63,7 @@ function splitSegmentsByLine(segments: StyledSegment[]): LineFragment[][] {
39
63
  const parts = segment.text.split('\n');
40
64
 
41
65
  parts.forEach((part, index) => {
42
- if (part.length > 0) {
66
+ if (part.length > 0 || segment.styles.imageSrc) {
43
67
  lines[lines.length - 1]?.push({
44
68
  text: part,
45
69
  styles: { ...segment.styles },
@@ -55,18 +79,48 @@ function splitSegmentsByLine(segments: StyledSegment[]): LineFragment[][] {
55
79
  return lines;
56
80
  }
57
81
 
58
- function serializeLine(
82
+ function serializeListBlock(
83
+ lines: LineFragment[][],
84
+ format: OutputFormat,
85
+ listType: ListType,
86
+ ): string {
87
+ if (format === 'html' || lines.some((line) => !!getLineTextAlign(line))) {
88
+ const tag = listType === 'ordered' ? 'ol' : 'ul';
89
+ const items = lines.map((line) => serializeHtmlListItem(line)).join('');
90
+ return `<${tag}>${items}</${tag}>`;
91
+ }
92
+
93
+ return lines
94
+ .map((line, index) => {
95
+ const marker = listType === 'ordered' ? `${index + 1}.` : '-';
96
+ const content = serializeLineContent(line, format);
97
+ return content.length > 0 ? `${marker} ${content}` : marker;
98
+ })
99
+ .join('\n');
100
+ }
101
+
102
+ function serializeHtmlListItem(line: LineFragment[]): string {
103
+ const content = serializeLineContent(line, 'html');
104
+ const styleAttribute = buildBlockStyle(getLineTextAlign(line));
105
+ return `<li${styleAttribute ? ` style="${styleAttribute}"` : ''}>${content}</li>`;
106
+ }
107
+
108
+ function serializeBlockLine(
59
109
  line: LineFragment[],
60
110
  format: OutputFormat,
61
111
  ): string {
62
112
  const heading = getLineHeading(line);
63
- const content = line
64
- .map((fragment) => serializeFragment(fragment, format, heading))
65
- .join('');
113
+ const textAlign = getLineTextAlign(line);
114
+ const content = serializeLineContent(line, format, heading);
66
115
 
67
116
  if (format === 'html') {
68
117
  const blockTag = heading ?? 'p';
69
- return `<${blockTag}>${content}</${blockTag}>`;
118
+ const styleAttribute = buildBlockStyle(textAlign);
119
+ return `<${blockTag}${styleAttribute ? ` style="${styleAttribute}"` : ''}>${content}</${blockTag}>`;
120
+ }
121
+
122
+ if (textAlign) {
123
+ return serializeAlignedMarkdownLine(content, heading, textAlign);
70
124
  }
71
125
 
72
126
  const headingPrefix = getHeadingPrefix(heading);
@@ -77,15 +131,42 @@ function serializeLine(
77
131
  return content.length > 0 ? `${headingPrefix} ${content}` : headingPrefix;
78
132
  }
79
133
 
134
+ function serializeAlignedMarkdownLine(
135
+ content: string,
136
+ heading: HeadingLevel | undefined,
137
+ textAlign: TextAlign,
138
+ ): string {
139
+ const blockTag = heading ?? 'p';
140
+ const styleAttribute = buildBlockStyle(textAlign);
141
+ return `<${blockTag} style="${styleAttribute}">${content}</${blockTag}>`;
142
+ }
143
+
144
+ function serializeLineContent(
145
+ line: LineFragment[],
146
+ format: OutputFormat,
147
+ lineHeading?: HeadingLevel,
148
+ ): string {
149
+ return line
150
+ .map((fragment) => serializeFragment(fragment, format, lineHeading))
151
+ .join('');
152
+ }
153
+
80
154
  function serializeFragment(
81
155
  fragment: LineFragment,
82
156
  format: OutputFormat,
83
157
  lineHeading?: HeadingLevel,
84
158
  ): string {
159
+ if (fragment.styles.imageSrc) {
160
+ return serializeImageFragment(fragment, format);
161
+ }
162
+
85
163
  const normalizedStyles: FormatStyle = {
86
164
  ...fragment.styles,
87
165
  heading: undefined,
88
- // Markdown headings already express emphasis at the block level.
166
+ listType: undefined,
167
+ textAlign: undefined,
168
+ imageSrc: undefined,
169
+ imageAlt: undefined,
89
170
  bold:
90
171
  lineHeading && lineHeading !== 'none' ? false : fragment.styles.bold,
91
172
  };
@@ -95,6 +176,20 @@ function serializeFragment(
95
176
  : serializeMarkdownFragment(fragment.text, normalizedStyles);
96
177
  }
97
178
 
179
+ function serializeImageFragment(
180
+ fragment: LineFragment,
181
+ format: OutputFormat,
182
+ ): string {
183
+ const source = fragment.styles.imageSrc ?? '';
184
+ const altText = fragment.styles.imageAlt ?? extractImageAlt(fragment.text);
185
+
186
+ if (format === 'html') {
187
+ return `<img src="${escapeHtml(source)}" alt="${escapeHtml(altText)}" />`;
188
+ }
189
+
190
+ return `![${escapeMarkdown(altText)}](${escapeMarkdownUrl(source)})`;
191
+ }
192
+
98
193
  function serializeHtmlFragment(text: string, styles: FormatStyle): string {
99
194
  let result = escapeHtml(text);
100
195
 
@@ -123,6 +218,10 @@ function serializeHtmlFragment(text: string, styles: FormatStyle): string {
123
218
  result = `<span style="${styleAttribute}">${result}</span>`;
124
219
  }
125
220
 
221
+ if (styles.link) {
222
+ result = `<a href="${escapeHtml(styles.link)}">${result}</a>`;
223
+ }
224
+
126
225
  return result;
127
226
  }
128
227
 
@@ -154,6 +253,10 @@ function serializeMarkdownFragment(text: string, styles: FormatStyle): string {
154
253
  result = `<span style="${styleAttribute}">${result}</span>`;
155
254
  }
156
255
 
256
+ if (styles.link) {
257
+ result = `[${result}](${escapeMarkdownUrl(styles.link)})`;
258
+ }
259
+
157
260
  return result;
158
261
  }
159
262
 
@@ -175,6 +278,16 @@ function buildInlineStyle(styles: FormatStyle): string {
175
278
  return cssRules.join('; ');
176
279
  }
177
280
 
281
+ function buildBlockStyle(textAlign?: TextAlign): string {
282
+ const cssRules: string[] = [];
283
+
284
+ if (textAlign) {
285
+ cssRules.push(`text-align: ${textAlign}`);
286
+ }
287
+
288
+ return cssRules.join('; ');
289
+ }
290
+
178
291
  function getLineHeading(line: LineFragment[]): HeadingLevel | undefined {
179
292
  for (const fragment of line) {
180
293
  if (fragment.styles.heading && fragment.styles.heading !== 'none') {
@@ -185,6 +298,26 @@ function getLineHeading(line: LineFragment[]): HeadingLevel | undefined {
185
298
  return undefined;
186
299
  }
187
300
 
301
+ function getLineListType(line: LineFragment[]): ListType | undefined {
302
+ for (const fragment of line) {
303
+ if (fragment.styles.listType && fragment.styles.listType !== 'none') {
304
+ return fragment.styles.listType;
305
+ }
306
+ }
307
+
308
+ return undefined;
309
+ }
310
+
311
+ function getLineTextAlign(line: LineFragment[]): TextAlign | undefined {
312
+ for (const fragment of line) {
313
+ if (fragment.styles.textAlign) {
314
+ return fragment.styles.textAlign;
315
+ }
316
+ }
317
+
318
+ return undefined;
319
+ }
320
+
188
321
  function getHeadingPrefix(heading?: HeadingLevel): string | undefined {
189
322
  switch (heading) {
190
323
  case 'h1':
@@ -198,6 +331,11 @@ function getHeadingPrefix(heading?: HeadingLevel): string | undefined {
198
331
  }
199
332
  }
200
333
 
334
+ function extractImageAlt(text: string): string {
335
+ const normalized = text.replace(/^\[Image:\s*/i, '').replace(/^\[Image\]/i, '').replace(/\]$/, '').trim();
336
+ return normalized.length > 0 ? normalized : 'image';
337
+ }
338
+
201
339
  function escapeHtml(text: string): string {
202
340
  return text
203
341
  .replaceAll('&', '&amp;')
@@ -211,6 +349,10 @@ function escapeMarkdown(text: string): string {
211
349
  return text.replace(/([\\`*_~[\]])/g, '\\$1');
212
350
  }
213
351
 
352
+ function escapeMarkdownUrl(url: string): string {
353
+ return url.replaceAll(' ', '%20').replaceAll(')', '%29');
354
+ }
355
+
214
356
  function wrapInlineCode(text: string): string {
215
357
  const matches = text.match(/`+/g);
216
358
  const longestBacktickRun = matches?.reduce(
@@ -55,6 +55,10 @@ export function formatStyleToTextStyle(
55
55
  style.fontSize = formatStyle.fontSize;
56
56
  }
57
57
 
58
+ if (formatStyle.textAlign) {
59
+ style.textAlign = formatStyle.textAlign;
60
+ }
61
+
58
62
  // Heading — overrides font size and weight
59
63
  if (formatStyle.heading && formatStyle.heading !== 'none') {
60
64
  style.fontSize = HEADING_FONT_SIZES[formatStyle.heading];
@@ -62,6 +66,23 @@ export function formatStyleToTextStyle(
62
66
  style.lineHeight = HEADING_FONT_SIZES[formatStyle.heading] * 1.3;
63
67
  }
64
68
 
69
+ if (formatStyle.link) {
70
+ style.color =
71
+ style.color ??
72
+ resolvedTheme.colors?.link ??
73
+ DEFAULT_THEME.colors?.link ??
74
+ DEFAULT_THEME.colors?.primary;
75
+
76
+ if (style.textDecorationLine === 'line-through') {
77
+ style.textDecorationLine = 'underline line-through';
78
+ } else if (
79
+ !style.textDecorationLine ||
80
+ style.textDecorationLine === 'none'
81
+ ) {
82
+ style.textDecorationLine = 'underline';
83
+ }
84
+ }
85
+
65
86
  return style;
66
87
  }
67
88