react-native-simple-rich-text-editor 0.1.0-beta.0
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.
- package/LICENSE +20 -0
- package/README.md +69 -0
- package/lib/commonjs/index.js +166 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/internal/rendering/components/Bold.js +23 -0
- package/lib/commonjs/internal/rendering/components/Bold.js.map +1 -0
- package/lib/commonjs/internal/rendering/components/BulletPoint.js +30 -0
- package/lib/commonjs/internal/rendering/components/BulletPoint.js.map +1 -0
- package/lib/commonjs/internal/rendering/components/Headline.js +37 -0
- package/lib/commonjs/internal/rendering/components/Headline.js.map +1 -0
- package/lib/commonjs/internal/rendering/components/Italic.js +23 -0
- package/lib/commonjs/internal/rendering/components/Italic.js.map +1 -0
- package/lib/commonjs/internal/rendering/components/Render.js +99 -0
- package/lib/commonjs/internal/rendering/components/Render.js.map +1 -0
- package/lib/commonjs/internal/rendering/utils.js +19 -0
- package/lib/commonjs/internal/rendering/utils.js.map +1 -0
- package/lib/commonjs/internal/text-formats/conversions.js +47 -0
- package/lib/commonjs/internal/text-formats/conversions.js.map +1 -0
- package/lib/commonjs/internal/text-formats/markdown-format.js +2 -0
- package/lib/commonjs/internal/text-formats/markdown-format.js.map +1 -0
- package/lib/commonjs/internal/text-formats/unicode-markers-format/constants.js +48 -0
- package/lib/commonjs/internal/text-formats/unicode-markers-format/constants.js.map +1 -0
- package/lib/commonjs/internal/text-formats/unicode-markers-format/encode-decode.js +65 -0
- package/lib/commonjs/internal/text-formats/unicode-markers-format/encode-decode.js.map +1 -0
- package/lib/commonjs/internal/text-formats/unicode-markers-format/markers.js +19 -0
- package/lib/commonjs/internal/text-formats/unicode-markers-format/markers.js.map +1 -0
- package/lib/commonjs/internal/text-formats/unicode-markers-format/text-manipulation.js +124 -0
- package/lib/commonjs/internal/text-formats/unicode-markers-format/text-manipulation.js.map +1 -0
- package/lib/module/index.js +162 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/internal/rendering/components/Bold.js +18 -0
- package/lib/module/internal/rendering/components/Bold.js.map +1 -0
- package/lib/module/internal/rendering/components/BulletPoint.js +25 -0
- package/lib/module/internal/rendering/components/BulletPoint.js.map +1 -0
- package/lib/module/internal/rendering/components/Headline.js +32 -0
- package/lib/module/internal/rendering/components/Headline.js.map +1 -0
- package/lib/module/internal/rendering/components/Italic.js +18 -0
- package/lib/module/internal/rendering/components/Italic.js.map +1 -0
- package/lib/module/internal/rendering/components/Render.js +94 -0
- package/lib/module/internal/rendering/components/Render.js.map +1 -0
- package/lib/module/internal/rendering/utils.js +16 -0
- package/lib/module/internal/rendering/utils.js.map +1 -0
- package/lib/module/internal/text-formats/conversions.js +43 -0
- package/lib/module/internal/text-formats/conversions.js.map +1 -0
- package/lib/module/internal/text-formats/markdown-format.js +2 -0
- package/lib/module/internal/text-formats/markdown-format.js.map +1 -0
- package/lib/module/internal/text-formats/unicode-markers-format/constants.js +45 -0
- package/lib/module/internal/text-formats/unicode-markers-format/constants.js.map +1 -0
- package/lib/module/internal/text-formats/unicode-markers-format/encode-decode.js +61 -0
- package/lib/module/internal/text-formats/unicode-markers-format/encode-decode.js.map +1 -0
- package/lib/module/internal/text-formats/unicode-markers-format/markers.js +15 -0
- package/lib/module/internal/text-formats/unicode-markers-format/markers.js.map +1 -0
- package/lib/module/internal/text-formats/unicode-markers-format/text-manipulation.js +117 -0
- package/lib/module/internal/text-formats/unicode-markers-format/text-manipulation.js.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +38 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/internal/rendering/components/Bold.d.ts +3 -0
- package/lib/typescript/commonjs/src/internal/rendering/components/Bold.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/internal/rendering/components/BulletPoint.d.ts +3 -0
- package/lib/typescript/commonjs/src/internal/rendering/components/BulletPoint.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/internal/rendering/components/Headline.d.ts +24 -0
- package/lib/typescript/commonjs/src/internal/rendering/components/Headline.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/internal/rendering/components/Italic.d.ts +3 -0
- package/lib/typescript/commonjs/src/internal/rendering/components/Italic.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/internal/rendering/components/Render.d.ts +4 -0
- package/lib/typescript/commonjs/src/internal/rendering/components/Render.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/internal/rendering/utils.d.ts +3 -0
- package/lib/typescript/commonjs/src/internal/rendering/utils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/internal/text-formats/conversions.d.ts +9 -0
- package/lib/typescript/commonjs/src/internal/text-formats/conversions.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/internal/text-formats/markdown-format.d.ts +1 -0
- package/lib/typescript/commonjs/src/internal/text-formats/markdown-format.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/internal/text-formats/unicode-markers-format/constants.d.ts +26 -0
- package/lib/typescript/commonjs/src/internal/text-formats/unicode-markers-format/constants.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/internal/text-formats/unicode-markers-format/encode-decode.d.ts +15 -0
- package/lib/typescript/commonjs/src/internal/text-formats/unicode-markers-format/encode-decode.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/internal/text-formats/unicode-markers-format/markers.d.ts +11 -0
- package/lib/typescript/commonjs/src/internal/text-formats/unicode-markers-format/markers.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/internal/text-formats/unicode-markers-format/text-manipulation.d.ts +52 -0
- package/lib/typescript/commonjs/src/internal/text-formats/unicode-markers-format/text-manipulation.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/index.d.ts +38 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/lib/typescript/module/src/internal/rendering/components/Bold.d.ts +3 -0
- package/lib/typescript/module/src/internal/rendering/components/Bold.d.ts.map +1 -0
- package/lib/typescript/module/src/internal/rendering/components/BulletPoint.d.ts +3 -0
- package/lib/typescript/module/src/internal/rendering/components/BulletPoint.d.ts.map +1 -0
- package/lib/typescript/module/src/internal/rendering/components/Headline.d.ts +24 -0
- package/lib/typescript/module/src/internal/rendering/components/Headline.d.ts.map +1 -0
- package/lib/typescript/module/src/internal/rendering/components/Italic.d.ts +3 -0
- package/lib/typescript/module/src/internal/rendering/components/Italic.d.ts.map +1 -0
- package/lib/typescript/module/src/internal/rendering/components/Render.d.ts +4 -0
- package/lib/typescript/module/src/internal/rendering/components/Render.d.ts.map +1 -0
- package/lib/typescript/module/src/internal/rendering/utils.d.ts +3 -0
- package/lib/typescript/module/src/internal/rendering/utils.d.ts.map +1 -0
- package/lib/typescript/module/src/internal/text-formats/conversions.d.ts +9 -0
- package/lib/typescript/module/src/internal/text-formats/conversions.d.ts.map +1 -0
- package/lib/typescript/module/src/internal/text-formats/markdown-format.d.ts +1 -0
- package/lib/typescript/module/src/internal/text-formats/markdown-format.d.ts.map +1 -0
- package/lib/typescript/module/src/internal/text-formats/unicode-markers-format/constants.d.ts +26 -0
- package/lib/typescript/module/src/internal/text-formats/unicode-markers-format/constants.d.ts.map +1 -0
- package/lib/typescript/module/src/internal/text-formats/unicode-markers-format/encode-decode.d.ts +15 -0
- package/lib/typescript/module/src/internal/text-formats/unicode-markers-format/encode-decode.d.ts.map +1 -0
- package/lib/typescript/module/src/internal/text-formats/unicode-markers-format/markers.d.ts +11 -0
- package/lib/typescript/module/src/internal/text-formats/unicode-markers-format/markers.d.ts.map +1 -0
- package/lib/typescript/module/src/internal/text-formats/unicode-markers-format/text-manipulation.d.ts +52 -0
- package/lib/typescript/module/src/internal/text-formats/unicode-markers-format/text-manipulation.d.ts.map +1 -0
- package/package.json +194 -0
- package/src/index.tsx +178 -0
- package/src/internal/rendering/components/Bold.tsx +12 -0
- package/src/internal/rendering/components/BulletPoint.tsx +19 -0
- package/src/internal/rendering/components/Headline.tsx +32 -0
- package/src/internal/rendering/components/Italic.tsx +12 -0
- package/src/internal/rendering/components/Render.tsx +105 -0
- package/src/internal/rendering/utils.ts +15 -0
- package/src/internal/text-formats/conversions.ts +62 -0
- package/src/internal/text-formats/markdown-format.ts +0 -0
- package/src/internal/text-formats/unicode-markers-format/constants.ts +64 -0
- package/src/internal/text-formats/unicode-markers-format/encode-decode.ts +60 -0
- package/src/internal/text-formats/unicode-markers-format/markers.ts +19 -0
- package/src/internal/text-formats/unicode-markers-format/text-manipulation.ts +135 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
import type { PropsWithChildren } from 'react';
|
2
|
+
import { Text, StyleSheet } from 'react-native';
|
3
|
+
|
4
|
+
export const Italic = ({ children }: PropsWithChildren) => {
|
5
|
+
return <Text style={styles.italic}>{children}</Text>;
|
6
|
+
};
|
7
|
+
|
8
|
+
const styles = StyleSheet.create({
|
9
|
+
italic: {
|
10
|
+
fontStyle: 'italic',
|
11
|
+
},
|
12
|
+
});
|
@@ -0,0 +1,105 @@
|
|
1
|
+
import { Text } from 'react-native';
|
2
|
+
import {
|
3
|
+
ZWS,
|
4
|
+
MARKER_STRING_LENGTH,
|
5
|
+
BULLET_POINT_STRING_REPRESENTATION,
|
6
|
+
} from '../../text-formats/unicode-markers-format/constants.ts';
|
7
|
+
import { Headline } from './Headline.tsx';
|
8
|
+
import { Bold } from './Bold.tsx';
|
9
|
+
import { Markers } from '../../text-formats/unicode-markers-format/markers.ts';
|
10
|
+
import { Fragment } from 'react/jsx-runtime';
|
11
|
+
import { Italic } from './Italic.tsx';
|
12
|
+
import { BulletPoint } from './BulletPoint.tsx';
|
13
|
+
import { intersperse } from '../utils.ts';
|
14
|
+
|
15
|
+
export const Render = ({ encodedText }: { encodedText: string }) => {
|
16
|
+
const lines = splitPreservingSeparator(
|
17
|
+
encodedText.replaceAll(BULLET_POINT_STRING_REPRESENTATION, ''),
|
18
|
+
ZWS
|
19
|
+
); // each BulletPoint.tsx instance adds a BULLET_POINT_STRING_REPRESENTATION, we need to remove them to avoid adding extra chars on each render
|
20
|
+
|
21
|
+
return lines.map((line, i) => {
|
22
|
+
const marker = line.substring(0, MARKER_STRING_LENGTH);
|
23
|
+
switch (marker) {
|
24
|
+
case Markers.H1:
|
25
|
+
case Markers.H2:
|
26
|
+
case Markers.H3:
|
27
|
+
const [headline, ...restOfLineWithoutMarkers] =
|
28
|
+
splitPreservingSeparator(line, '\n');
|
29
|
+
return (
|
30
|
+
<Fragment key={i + line}>
|
31
|
+
<Headline level={marker}>{headline}</Headline>
|
32
|
+
{restOfLineWithoutMarkers.join('')}
|
33
|
+
</Fragment>
|
34
|
+
);
|
35
|
+
case Markers.UL:
|
36
|
+
// NOTE: no other markers are allowed in a bullet point list for now..
|
37
|
+
// if there are other markers they will lead to a break out of the the list -> will make the list terminate early
|
38
|
+
const [bulletPoints, ...paragraphs] = splitPreservingSeparator(
|
39
|
+
line,
|
40
|
+
'\n\n'
|
41
|
+
);
|
42
|
+
|
43
|
+
return (
|
44
|
+
<Fragment key={i + line}>
|
45
|
+
{intersperse(
|
46
|
+
// adds newlines after split() operation
|
47
|
+
(bulletPoints ?? '')
|
48
|
+
.split('\n') // erases newlines
|
49
|
+
.map((bulletPointLine, j) => (
|
50
|
+
<BulletPoint key={j + bulletPointLine}>
|
51
|
+
{bulletPointLine}
|
52
|
+
</BulletPoint>
|
53
|
+
)),
|
54
|
+
'\n'
|
55
|
+
)}
|
56
|
+
{paragraphs.map((p) => (
|
57
|
+
<Text key={p}>{p}</Text>
|
58
|
+
))}
|
59
|
+
</Fragment>
|
60
|
+
);
|
61
|
+
case Markers.BOLD_START:
|
62
|
+
// NOTE: We could check if lines[i+1] starts with a BOLD_END marker
|
63
|
+
// but for now I opt for this simpler solution.
|
64
|
+
// Because of the above: There must not be any other markers between Markers.BOLD_START and Markers.BOLD_END!
|
65
|
+
return <Bold key={i + line}>{line}</Bold>;
|
66
|
+
case Markers.BOLD_END:
|
67
|
+
break;
|
68
|
+
case Markers.ITALIC_START:
|
69
|
+
return <Italic key={i + line}>{line}</Italic>;
|
70
|
+
case Markers.ITALIC_END:
|
71
|
+
break;
|
72
|
+
default:
|
73
|
+
// remove any broken markers
|
74
|
+
// markers can be broken by erasing text via backspace key in the editor
|
75
|
+
// first line always has no marker at the start
|
76
|
+
if (i !== 0) {
|
77
|
+
// detect broken marker
|
78
|
+
if (line.length < MARKER_STRING_LENGTH) {
|
79
|
+
return '';
|
80
|
+
}
|
81
|
+
}
|
82
|
+
break;
|
83
|
+
}
|
84
|
+
|
85
|
+
return <Text key={i + line}>{line}</Text>;
|
86
|
+
});
|
87
|
+
};
|
88
|
+
|
89
|
+
/**
|
90
|
+
* Splits a string by the specified separator but preserves the separator
|
91
|
+
* in the resulting array.
|
92
|
+
*
|
93
|
+
* @param str - The string to split
|
94
|
+
* @param separator - The separator to split by (string or RegExp)
|
95
|
+
* @returns An array of strings split by the separator with special handling
|
96
|
+
*/
|
97
|
+
function splitPreservingSeparator(str: string, separator: string): string[] {
|
98
|
+
return str.split(separator).map((s, i) => {
|
99
|
+
if (i === 0) {
|
100
|
+
return s;
|
101
|
+
} else {
|
102
|
+
return separator + s;
|
103
|
+
}
|
104
|
+
});
|
105
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
// add the separator after each element of arr, except for the last element
|
2
|
+
function intersperse<T>(arr: T[], separator: T | string): (T | string)[] {
|
3
|
+
if (arr.length === 0) return [];
|
4
|
+
|
5
|
+
const output = new Array(arr.length * 2 - 1);
|
6
|
+
for (let i = 0, j = 0; i < arr.length; i++, j += 2) {
|
7
|
+
output[j] = arr[i];
|
8
|
+
if (j + 1 < output.length) {
|
9
|
+
output[j + 1] = separator;
|
10
|
+
}
|
11
|
+
}
|
12
|
+
return output;
|
13
|
+
}
|
14
|
+
|
15
|
+
export { intersperse };
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import {
|
2
|
+
HEADLINE_LEVELS,
|
3
|
+
HEADLINE_MARKER_TYPE,
|
4
|
+
ZWS,
|
5
|
+
MARKER_BYTE_LENGTH,
|
6
|
+
} from './unicode-markers-format/constants.ts';
|
7
|
+
import { decode } from './unicode-markers-format/encode-decode.ts';
|
8
|
+
|
9
|
+
const Markdown = {
|
10
|
+
H1: '# ',
|
11
|
+
H2: '## ',
|
12
|
+
H3: '### ',
|
13
|
+
} as const;
|
14
|
+
|
15
|
+
type MarkdownString = string;
|
16
|
+
|
17
|
+
/**
|
18
|
+
* convertInternalFormatToMarkdown converts text from the internal format to Markdown
|
19
|
+
* @param internalText rich text in the internal format
|
20
|
+
* @returns markdown formatted text
|
21
|
+
*/
|
22
|
+
const convertInternalFormatToMarkdown = (
|
23
|
+
internalText: string
|
24
|
+
): MarkdownString => {
|
25
|
+
// // 1. find all markers
|
26
|
+
// // 2. translate the markers into corresponding Markdown markers
|
27
|
+
// // 3. Replace internal markers with Markdown markers
|
28
|
+
let markdownText = '';
|
29
|
+
|
30
|
+
let i = 0;
|
31
|
+
while (i < internalText.length) {
|
32
|
+
const char = internalText[i];
|
33
|
+
|
34
|
+
if (char === ZWS) {
|
35
|
+
markdownText += decodedTokenToMarkdownToken(
|
36
|
+
decode(internalText.substring(i, i + MARKER_BYTE_LENGTH))
|
37
|
+
);
|
38
|
+
i += MARKER_BYTE_LENGTH;
|
39
|
+
} else {
|
40
|
+
markdownText += char;
|
41
|
+
i++;
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
return markdownText;
|
46
|
+
};
|
47
|
+
|
48
|
+
const decodedTokenToMarkdownToken = (
|
49
|
+
internalToken: number[]
|
50
|
+
): MarkdownString => {
|
51
|
+
// the first byte declares the token type
|
52
|
+
switch (internalToken[0]) {
|
53
|
+
case HEADLINE_MARKER_TYPE:
|
54
|
+
return Markdown[
|
55
|
+
`H${internalToken[1] as (typeof HEADLINE_LEVELS)[keyof typeof HEADLINE_LEVELS]}`
|
56
|
+
];
|
57
|
+
default:
|
58
|
+
return '';
|
59
|
+
}
|
60
|
+
};
|
61
|
+
|
62
|
+
export { convertInternalFormatToMarkdown };
|
File without changes
|
@@ -0,0 +1,64 @@
|
|
1
|
+
/**
|
2
|
+
* Constants for rich text markers.
|
3
|
+
*/
|
4
|
+
const ZWS = '\u200B'; // Zero-width space used as the marker (both as prefix and base).
|
5
|
+
const MARKER_BYTE_LENGTH = 6; // Define a fixed marker length enables easy decoding when iterating over a string of the internal text format
|
6
|
+
const MARKER_STRING_LENGTH = MARKER_BYTE_LENGTH + 2; // like Markers.H1.length or Markers.BOLD.length
|
7
|
+
const HEADLINE_MARKER_TYPE = 0x10; // Marker type for headline start.
|
8
|
+
// PROBLEM: HEADLINE_LEVELS is used in two semantic ways. 1) for specifying the headline level in the encoded marker (see below) 2) and at the level interface (e.g. of Headline component) --> here we should use "H1" | "H2" | "H3" in an Enum or readonly object
|
9
|
+
const HEADLINE_LEVELS = {
|
10
|
+
H1: 1,
|
11
|
+
H2: 2,
|
12
|
+
H3: 3,
|
13
|
+
} as const;
|
14
|
+
const HEADLINE_MARKERS: Record<keyof typeof HEADLINE_LEVELS, Uint8Array> = {
|
15
|
+
H1: createMarker([HEADLINE_MARKER_TYPE, HEADLINE_LEVELS.H1]),
|
16
|
+
H2: createMarker([HEADLINE_MARKER_TYPE, HEADLINE_LEVELS.H2]),
|
17
|
+
H3: createMarker([HEADLINE_MARKER_TYPE, HEADLINE_LEVELS.H3]),
|
18
|
+
} as const;
|
19
|
+
|
20
|
+
const BOLD_MARKER_TYPE = 0x20;
|
21
|
+
const BOLD_MARKER_START = 1;
|
22
|
+
const BOLD_MARKER_END = 2;
|
23
|
+
|
24
|
+
const BOLD_MARKERS = {
|
25
|
+
START: createMarker([BOLD_MARKER_TYPE, BOLD_MARKER_START]),
|
26
|
+
END: createMarker([BOLD_MARKER_TYPE, BOLD_MARKER_END]),
|
27
|
+
} as const;
|
28
|
+
|
29
|
+
const ITALIC_MARKER_TYPE = 0x30;
|
30
|
+
const ITALIC_MARKER_START = 3;
|
31
|
+
const ITALIC_MARKER_END = 4;
|
32
|
+
|
33
|
+
const ITALIC_MARKERS = {
|
34
|
+
START: createMarker([ITALIC_MARKER_TYPE, ITALIC_MARKER_START]),
|
35
|
+
END: createMarker([ITALIC_MARKER_TYPE, ITALIC_MARKER_END]),
|
36
|
+
} as const;
|
37
|
+
|
38
|
+
const BULLET_POINT_MARKER_TYPE = 0x40;
|
39
|
+
const BULLET_POINT_UNORDERED = 1;
|
40
|
+
const BULLET_POINT_UL_MARKER = createMarker([
|
41
|
+
BULLET_POINT_MARKER_TYPE,
|
42
|
+
BULLET_POINT_UNORDERED,
|
43
|
+
]);
|
44
|
+
const BULLET_POINT_STRING_REPRESENTATION = '• ';
|
45
|
+
|
46
|
+
function createMarker(data: number[], length = MARKER_BYTE_LENGTH) {
|
47
|
+
const array = new Uint8Array(length);
|
48
|
+
array.set(data);
|
49
|
+
return array;
|
50
|
+
}
|
51
|
+
|
52
|
+
export {
|
53
|
+
ZWS,
|
54
|
+
MARKER_STRING_LENGTH,
|
55
|
+
MARKER_BYTE_LENGTH,
|
56
|
+
HEADLINE_MARKER_TYPE,
|
57
|
+
HEADLINE_LEVELS,
|
58
|
+
HEADLINE_MARKERS,
|
59
|
+
BOLD_MARKER_TYPE,
|
60
|
+
BOLD_MARKERS,
|
61
|
+
ITALIC_MARKERS,
|
62
|
+
BULLET_POINT_UL_MARKER,
|
63
|
+
BULLET_POINT_STRING_REPRESENTATION,
|
64
|
+
};
|
@@ -0,0 +1,60 @@
|
|
1
|
+
/**
|
2
|
+
* The encode function leverages a concept from the Unicode spec - Variant Selectors - which enables arbitrary bytes to be appended invisibly to a unicode char.
|
3
|
+
* @param base string, usually a single char, to which bytes are appended. These bytes remain invisible when base is rendered
|
4
|
+
* @param bytes to added to the base string/char.
|
5
|
+
* @returns string
|
6
|
+
*/
|
7
|
+
function encode(base: string, bytes: Uint8Array): string {
|
8
|
+
let result = base;
|
9
|
+
for (const byte of bytes) {
|
10
|
+
result += byteToVariationSelector(byte);
|
11
|
+
}
|
12
|
+
return result;
|
13
|
+
}
|
14
|
+
|
15
|
+
/**
|
16
|
+
* The decode function extracts data hidden inside a string masquerading as variation selectors.
|
17
|
+
* @param variationSelectors string that may contain bytes hidden as variation selectors
|
18
|
+
* @returns a list of numbers/bytes
|
19
|
+
*/
|
20
|
+
function decode(variationSelectors: string): number[] {
|
21
|
+
const result: number[] = [];
|
22
|
+
let started = false;
|
23
|
+
|
24
|
+
for (const char of variationSelectors) {
|
25
|
+
const byte = variationSelectorToByte(char);
|
26
|
+
if (byte !== null) {
|
27
|
+
result.push(byte);
|
28
|
+
started = true;
|
29
|
+
} else if (started) {
|
30
|
+
break;
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
return result;
|
35
|
+
}
|
36
|
+
|
37
|
+
// Utility function to transform a byte into a variation selector, for appending it to a unicode char.
|
38
|
+
function byteToVariationSelector(byte: number): string {
|
39
|
+
if (byte < 16) {
|
40
|
+
return String.fromCodePoint(0xfe00 + byte);
|
41
|
+
} else {
|
42
|
+
return String.fromCodePoint(0xe0100 + (byte - 16));
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
// Utility function to extract hidden data from a unicode char's variation selectors.
|
47
|
+
function variationSelectorToByte(variationSelector: string): number | null {
|
48
|
+
const codePoint = variationSelector.codePointAt(0);
|
49
|
+
if (codePoint === undefined) return null;
|
50
|
+
if (codePoint >= 0xfe00 && codePoint <= 0xfe0f) {
|
51
|
+
return codePoint - 0xfe00;
|
52
|
+
} else if (codePoint >= 0xe0100 && codePoint <= 0xe01ef) {
|
53
|
+
return codePoint - 0xe0100 + 16;
|
54
|
+
} else {
|
55
|
+
return null;
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
// Source of encode, decode and variation selector utilities is https://paulbutler.org/2025/smuggling-arbitrary-data-through-an-emoji/ originally written in Rust
|
60
|
+
export { encode, decode };
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import { encode } from './encode-decode.ts';
|
2
|
+
import {
|
3
|
+
ZWS,
|
4
|
+
HEADLINE_MARKERS,
|
5
|
+
BOLD_MARKERS,
|
6
|
+
ITALIC_MARKERS,
|
7
|
+
BULLET_POINT_UL_MARKER,
|
8
|
+
} from './constants.ts';
|
9
|
+
|
10
|
+
export const Markers = {
|
11
|
+
H1: encode(ZWS, HEADLINE_MARKERS.H1),
|
12
|
+
H2: encode(ZWS, HEADLINE_MARKERS.H2),
|
13
|
+
H3: encode(ZWS, HEADLINE_MARKERS.H3),
|
14
|
+
BOLD_START: encode(ZWS, BOLD_MARKERS.START),
|
15
|
+
BOLD_END: encode(ZWS, BOLD_MARKERS.END),
|
16
|
+
ITALIC_START: encode(ZWS, ITALIC_MARKERS.START),
|
17
|
+
ITALIC_END: encode(ZWS, ITALIC_MARKERS.END),
|
18
|
+
UL: encode(ZWS, BULLET_POINT_UL_MARKER),
|
19
|
+
} as const;
|
@@ -0,0 +1,135 @@
|
|
1
|
+
import { Markers } from './markers';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Adds a bullet point marker to the text at the current cursor position.
|
5
|
+
* If the cursor is not at the beginning of a line, a newline is added before the marker.
|
6
|
+
* If the cursor is at the beginning of a line, the marker is added directly.
|
7
|
+
*/
|
8
|
+
const addBulletPointMarker = (
|
9
|
+
text: string,
|
10
|
+
selection: { start: number; end: number }
|
11
|
+
) => {
|
12
|
+
if (!text) {
|
13
|
+
return Markers.UL;
|
14
|
+
}
|
15
|
+
|
16
|
+
// Split text at the current selection.
|
17
|
+
const { before, selected, after } = splitBySelection(text, selection);
|
18
|
+
|
19
|
+
// Ensure that the marker begins on a new line.
|
20
|
+
let prefix = '';
|
21
|
+
if (before.length > 0 && !before.endsWith('\n')) {
|
22
|
+
prefix = '\n';
|
23
|
+
}
|
24
|
+
|
25
|
+
// Append the marker
|
26
|
+
const insertion = prefix + Markers.UL;
|
27
|
+
return before + insertion + selected + after;
|
28
|
+
};
|
29
|
+
|
30
|
+
/**
|
31
|
+
* Inserts a headline start marker into the text at the current cursor position.
|
32
|
+
* The marker encodes two bytes:
|
33
|
+
* - Byte 0: marker type (0x10 for headline)
|
34
|
+
* - Byte 1: headline level (here: 1)
|
35
|
+
*
|
36
|
+
* The marker is inserted so that it starts on a new line.
|
37
|
+
*/
|
38
|
+
const addHeadlineMarker = (
|
39
|
+
marker: string, // marker already contains the headline level encoded
|
40
|
+
text: string,
|
41
|
+
selection: { start: number; end: number }
|
42
|
+
) => {
|
43
|
+
if (!text) {
|
44
|
+
return marker;
|
45
|
+
}
|
46
|
+
// Split text at the current selection.
|
47
|
+
const { before, after } = splitBySelection(text, selection);
|
48
|
+
|
49
|
+
// Do not add multiple consecutive markers, ZWS is the basis of each marker
|
50
|
+
// if (before.endsWith(ZWS) || after.startsWith(ZWS)) {
|
51
|
+
// return before + after;
|
52
|
+
// }
|
53
|
+
|
54
|
+
// Ensure that the marker begins on a new line.
|
55
|
+
let prefix = '';
|
56
|
+
if (!before.endsWith('\n')) {
|
57
|
+
prefix = '\n';
|
58
|
+
}
|
59
|
+
// Append the marker followed by a newline so that following text starts on a new line.
|
60
|
+
const insertion = prefix + marker;
|
61
|
+
return before + insertion + after;
|
62
|
+
};
|
63
|
+
|
64
|
+
const FontStyleMarkers: Record<
|
65
|
+
'bold' | 'italic',
|
66
|
+
{ START: string; END: string }
|
67
|
+
> = {
|
68
|
+
bold: {
|
69
|
+
START: Markers.BOLD_START,
|
70
|
+
END: Markers.BOLD_END,
|
71
|
+
},
|
72
|
+
italic: { START: Markers.ITALIC_START, END: Markers.ITALIC_END },
|
73
|
+
};
|
74
|
+
|
75
|
+
const addFontStyleEndMarker = (
|
76
|
+
text: string,
|
77
|
+
selection: { start: number; end: number },
|
78
|
+
style: 'bold' | 'italic'
|
79
|
+
) => {
|
80
|
+
const { before, selected, after } = splitBySelection(text, selection);
|
81
|
+
return {
|
82
|
+
text: before + selected + FontStyleMarkers[style].END + after,
|
83
|
+
selection,
|
84
|
+
};
|
85
|
+
};
|
86
|
+
|
87
|
+
const addFontStyleMarkers = (
|
88
|
+
text: string,
|
89
|
+
selection: { start: number; end: number },
|
90
|
+
style: 'bold' | 'italic'
|
91
|
+
) => {
|
92
|
+
const { before, selected, after } = splitBySelection(text, selection);
|
93
|
+
|
94
|
+
// effectively no selection
|
95
|
+
if (selection.end - selection.start === 0) {
|
96
|
+
let prefix = '';
|
97
|
+
if (!before.endsWith('\n') && !before.endsWith(' ')) {
|
98
|
+
prefix += ' ';
|
99
|
+
}
|
100
|
+
|
101
|
+
return {
|
102
|
+
text: before + prefix + FontStyleMarkers[style].START,
|
103
|
+
selection,
|
104
|
+
};
|
105
|
+
}
|
106
|
+
|
107
|
+
return {
|
108
|
+
text:
|
109
|
+
before +
|
110
|
+
FontStyleMarkers[style].START +
|
111
|
+
selected +
|
112
|
+
FontStyleMarkers[style].END +
|
113
|
+
after,
|
114
|
+
selection,
|
115
|
+
};
|
116
|
+
};
|
117
|
+
|
118
|
+
function splitBySelection(
|
119
|
+
text: string,
|
120
|
+
selection: { start: number; end: number }
|
121
|
+
): { before: string; selected: string; after: string } {
|
122
|
+
return {
|
123
|
+
before: text.substring(0, selection.start),
|
124
|
+
selected: text.substring(selection.start, selection.end),
|
125
|
+
after: text.substring(selection.end),
|
126
|
+
};
|
127
|
+
}
|
128
|
+
|
129
|
+
export {
|
130
|
+
addBulletPointMarker,
|
131
|
+
addHeadlineMarker,
|
132
|
+
addFontStyleMarkers,
|
133
|
+
addFontStyleEndMarker,
|
134
|
+
splitBySelection,
|
135
|
+
};
|