@apollohg/react-native-rich-text-editor 2.0.0 → 2.0.2
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/README.md +8 -116
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/android/build.gradle +3 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextImages.kt +25 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2RenderValidation.kt +1 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +101 -33
- package/android/src/main/java/com/apollohg/editor/RenderMediaSpans.kt +15 -3
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +1 -1
- package/app.plugin.js +42 -39
- package/dist/AtomHost.js +18 -11
- package/dist/EditorAddon.js +4 -2
- package/dist/EditorAddonNormalization.js +17 -9
- package/dist/EditorMentionThemeNormalization.js +6 -3
- package/dist/EditorMentionThemeValidation.js +28 -11
- package/dist/EditorStyleSheetNormalization.js +85 -38
- package/dist/EditorStyleSheetTypes.d.ts +1 -1
- package/dist/EditorTheme.d.ts +4 -0
- package/dist/EditorTheme.js +5 -3
- package/dist/EditorToolbarComponent.d.ts +1 -1
- package/dist/EditorToolbarComponent.js +1 -1
- package/dist/EditorToolbarRegistry.js +5 -5
- package/dist/EditorToolbarTypes.d.ts +12 -12
- package/dist/EditorToolbarVisuals.js +1 -1
- package/dist/ExternalTextComposition.js +89 -43
- package/dist/ImageLoadingPolicy.d.ts +1 -1
- package/dist/NativeEditorBoundaryError.d.ts +7 -7
- package/dist/NativeEditorBoundaryError.js +9 -5
- package/dist/NativeEditorCollaborationTransport.d.ts +7 -7
- package/dist/NativeEditorCollaborationTransport.js +28 -15
- package/dist/NativeEditorCreateConfig.js +11 -6
- package/dist/NativeEditorCreateJson.js +51 -25
- package/dist/NativeEditorCreateValidation.js +11 -6
- package/dist/NativeEditorDocumentBridge.d.ts +1 -1
- package/dist/NativeEditorDocumentBridge.js +32 -18
- package/dist/NativeEditorDocumentHandle.d.ts +4 -4
- package/dist/NativeEditorDocumentHandle.js +1 -1
- package/dist/NativeEditorLocalAwareness.js +25 -13
- package/dist/NativeEditorNativeModule.d.ts +8 -1
- package/dist/NativeEditorNativeModule.js +7 -5
- package/dist/NativeEditorRenderNormalization.js +66 -25
- package/dist/NativeEditorResultNormalization.js +53 -29
- package/dist/NativeEditorTypes.d.ts +10 -10
- package/dist/NativeEditorV2Decimal.js +6 -3
- package/dist/NativeProseViewer.js +11 -3
- package/dist/ResourceLimits.d.ts +2 -2
- package/dist/ResourceLimits.js +7 -2
- package/dist/RichTextEditor.js +1 -1
- package/dist/RichTextEditorNativeTypes.d.ts +1 -1
- package/dist/RichTextEditorNativeView.d.ts +1 -3
- package/dist/RichTextEditorSerialization.js +24 -14
- package/dist/RichTextEditorTypes.d.ts +1 -1
- package/dist/YjsCollaboration.d.ts +7 -7
- package/dist/YjsCollaboration.js +42 -26
- package/dist/addons.d.ts +4 -4
- package/dist/addons.js +3 -3
- package/dist/atomInstances.js +6 -3
- package/dist/atomUpdates.js +7 -4
- package/dist/atoms.js +20 -12
- package/dist/attributeValidation.js +55 -24
- package/dist/contentExpression.js +80 -42
- package/dist/listMarkers.d.ts +1 -1
- package/dist/listMarkers.js +1 -1
- package/dist/schemaDefinition.d.ts +1 -1
- package/dist/schemaDefinition.js +23 -14
- package/dist/schemaDocument.js +1 -1
- package/dist/schemaDocumentConstruction.js +25 -14
- package/dist/schemaNormalization.js +51 -29
- package/dist/schemaPresets.d.ts +1 -1
- package/dist/schemaPresets.js +9 -4
- package/dist/schemaResolution.js +46 -25
- package/dist/specs/PreparedProseViewerNativeComponent.d.ts +4 -0
- package/dist/useEditorToolbarInteractions.d.ts +2 -2
- package/dist/useEditorToolbarInteractions.js +45 -23
- package/dist/useEditorToolbarItems.d.ts +1 -1
- package/dist/useEditorToolbarItems.js +4 -3
- package/dist/useEditorToolbarPresentation.d.ts +3 -3
- package/dist/useEditorToolbarPresentation.js +4 -4
- package/dist/useEditorToolbarState.d.ts +1 -1
- package/dist/useEditorToolbarState.js +1 -1
- package/dist/useFocusPreservingFrames.js +11 -8
- package/dist/useNativeEditor.d.ts +1 -1
- package/dist/useNativeEditor.js +21 -11
- package/dist/useRichTextEditorCommands.d.ts +2 -2
- package/dist/useRichTextEditorCommands.js +59 -29
- package/dist/useRichTextEditorEvents.d.ts +3 -3
- package/dist/useRichTextEditorEvents.js +75 -32
- package/dist/useRichTextEditorMentions.d.ts +4 -4
- package/dist/useRichTextEditorMentions.js +48 -29
- package/dist/useRichTextEditorPresentation.d.ts +4 -4
- package/dist/useRichTextEditorPresentation.js +25 -23
- package/dist/useRichTextEditorState.d.ts +3 -3
- package/dist/useRichTextEditorState.js +20 -15
- package/dist/useRichTextEditorUpdates.d.ts +1 -1
- package/dist/useRichTextEditorUpdates.js +99 -41
- package/dist/useViewerAtoms.js +63 -36
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorInteractionOverlays.swift +114 -122
- package/ios/EditorLayoutManager.swift +13 -3
- package/ios/EditorStyleSheet.swift +12 -1
- package/ios/EditorTextView+Images.swift +32 -10
- package/ios/EditorTextView.swift +1 -1
- package/ios/EditorV2Adapter+RenderValidation.swift +2 -2
- package/ios/NativeToolbar+ButtonStyle.swift +17 -5
- package/ios/NativeToolbar+Diagnostics.swift +15 -0
- package/ios/RenderBridge+Atoms.swift +0 -1
- package/ios/RenderBridge+Attachments.swift +0 -29
- package/ios/RichTextEditorView.swift +4 -12
- package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +3 -2
- package/package.json +11 -3
- package/react-native.config.js +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/src/AtomHost.tsx +58 -28
- package/src/EditorAddon.ts +18 -8
- package/src/EditorAddonNormalization.ts +40 -11
- package/src/EditorMentionThemeNormalization.ts +15 -4
- package/src/EditorMentionThemeValidation.ts +51 -14
- package/src/EditorStyleSheet.ts +1 -0
- package/src/EditorStyleSheetNormalization.ts +195 -64
- package/src/EditorStyleSheetTypes.ts +1 -1
- package/src/EditorTheme.ts +13 -5
- package/src/EditorToolbarComponent.tsx +2 -1
- package/src/EditorToolbarRegistry.tsx +23 -13
- package/src/EditorToolbarTypes.tsx +12 -12
- package/src/EditorToolbarVisuals.tsx +6 -3
- package/src/EditorUpdateRevision.ts +1 -0
- package/src/ExternalTextComposition.ts +259 -49
- package/src/ImageLoadingPolicy.ts +3 -1
- package/src/NativeEditorBoundaryError.ts +38 -11
- package/src/NativeEditorCollaborationTransport.ts +108 -27
- package/src/NativeEditorCreateConfig.ts +92 -19
- package/src/NativeEditorCreateJson.ts +163 -26
- package/src/NativeEditorCreateValidation.ts +33 -12
- package/src/NativeEditorDocumentBridge.ts +111 -19
- package/src/NativeEditorDocumentHandle.ts +9 -4
- package/src/NativeEditorLocalAwareness.ts +70 -15
- package/src/NativeEditorNativeModule.ts +13 -9
- package/src/NativeEditorRenderNormalization.ts +128 -29
- package/src/NativeEditorResultNormalization.ts +143 -32
- package/src/NativeEditorTypes.ts +12 -11
- package/src/NativeEditorV2Decimal.ts +12 -3
- package/src/NativeProseViewer.tsx +46 -16
- package/src/ResourceLimits.ts +15 -8
- package/src/RichTextEditor.tsx +3 -1
- package/src/RichTextEditorNativeTypes.tsx +1 -1
- package/src/RichTextEditorNativeView.tsx +2 -8
- package/src/RichTextEditorSerialization.tsx +70 -24
- package/src/RichTextEditorTypes.tsx +1 -1
- package/src/YjsCollaboration.ts +143 -47
- package/src/addons.ts +12 -8
- package/src/atomInstances.ts +25 -5
- package/src/atomUpdates.ts +23 -6
- package/src/atoms.ts +85 -18
- package/src/attributeValidation.ts +97 -33
- package/src/contentExpression.ts +218 -63
- package/src/listMarkers.ts +1 -1
- package/src/schemaDefinition.ts +82 -20
- package/src/schemaDocument.ts +15 -1
- package/src/schemaDocumentConstruction.ts +80 -16
- package/src/schemaNormalization.ts +155 -41
- package/src/schemaPresets.ts +37 -31
- package/src/schemaResolution.ts +126 -27
- package/src/specs/PreparedProseViewerNativeComponent.ts +4 -2
- package/src/useEditorToolbarInteractions.tsx +85 -36
- package/src/useEditorToolbarItems.tsx +50 -22
- package/src/useEditorToolbarPresentation.tsx +81 -58
- package/src/useEditorToolbarState.tsx +8 -6
- package/src/useFocusPreservingFrames.ts +43 -15
- package/src/useNativeEditor.ts +111 -51
- package/src/useRichTextEditorCommands.tsx +127 -54
- package/src/useRichTextEditorEvents.tsx +144 -42
- package/src/useRichTextEditorMentions.tsx +118 -37
- package/src/useRichTextEditorPresentation.tsx +128 -114
- package/src/useRichTextEditorState.tsx +76 -40
- package/src/useRichTextEditorUpdates.tsx +207 -56
- package/src/useViewerAtoms.tsx +162 -66
|
@@ -14,36 +14,65 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function normalizeEditorAddons(addons?: EditorAddons): NormalizedEditorAddons {
|
|
17
|
-
if (addons === undefined)
|
|
18
|
-
|
|
17
|
+
if (addons === undefined) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!Array.isArray(addons)) {
|
|
22
|
+
throw new Error('addons must be a flat readonly array.');
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
const normalized: NormalizedEditorAddons = {};
|
|
20
26
|
const ids = new Set<string>();
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
|
|
28
|
+
for (const [ index, addon ] of addons.entries()) {
|
|
29
|
+
if (addon === false || addon === null || addon === undefined) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
const path = `addons[${index}]`;
|
|
24
|
-
|
|
25
|
-
if (addon
|
|
34
|
+
|
|
35
|
+
if (!isRecord(addon)) {
|
|
36
|
+
throw new Error(`${path} must be an addon descriptor.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (addon.version !== 1) {
|
|
40
|
+
throw new Error(`${path}.version must be 1.`);
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
if (addon.capability !== 'mentions' && addon.capability !== 'code-highlighting') {
|
|
27
44
|
throw new Error(`${path}.capability is unsupported.`);
|
|
28
45
|
}
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
throw new Error(`${path}
|
|
46
|
+
|
|
47
|
+
if (addon.id !== addon.capability) {
|
|
48
|
+
throw new Error(`${path}.id must match its capability.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (ids.has(addon.capability)) {
|
|
52
|
+
throw new Error(`${path}: duplicate addon id '${String(addon.id)}'.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
32
55
|
ids.add(addon.capability);
|
|
33
|
-
|
|
56
|
+
|
|
57
|
+
if (!isRecord(addon.options)) {
|
|
58
|
+
throw new Error(`${path}.options must be an object.`);
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
if (addon.capability === 'mentions') {
|
|
35
62
|
normalized.mentions = addon.options;
|
|
36
63
|
} else {
|
|
37
|
-
for (const key of ['provider', 'theme'] as const) {
|
|
64
|
+
for (const key of [ 'provider', 'theme' ] as const) {
|
|
38
65
|
if (typeof addon.options[key] !== 'string' || !addon.options[key].trim()) {
|
|
39
66
|
throw new Error(`${path}.options.${key} must be a nonempty string.`);
|
|
40
67
|
}
|
|
41
68
|
}
|
|
69
|
+
|
|
42
70
|
normalized.codeHighlighting = {
|
|
43
71
|
provider: addon.options.provider as string,
|
|
44
72
|
theme: addon.options.theme as string,
|
|
45
73
|
};
|
|
46
74
|
}
|
|
47
75
|
}
|
|
76
|
+
|
|
48
77
|
return normalized;
|
|
49
78
|
}
|
|
@@ -10,14 +10,25 @@ export type SerializedEditorMentionTheme = Omit<EditorMentionTheme, 'node'> & {
|
|
|
10
10
|
export function normalizeEditorMentionTheme(
|
|
11
11
|
theme?: EditorMentionTheme
|
|
12
12
|
): SerializedEditorMentionTheme | undefined {
|
|
13
|
-
if (theme === undefined)
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
if (theme === undefined) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!validEditorMentionTheme(theme)) {
|
|
18
|
+
throw new TypeError('Invalid mention theme');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!theme.node) {
|
|
22
|
+
return theme;
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
const node = theme.node as Record<string, unknown>;
|
|
17
26
|
const { textColor, style: existingStyle, ...appearance } = node;
|
|
27
|
+
|
|
18
28
|
const style = normalizeEditorTheme({
|
|
19
|
-
mention: [{ color: textColor }, appearance, existingStyle],
|
|
29
|
+
mention: [ { color: textColor }, appearance, existingStyle ],
|
|
20
30
|
}).styles?.mention;
|
|
31
|
+
|
|
21
32
|
return {
|
|
22
33
|
...theme,
|
|
23
34
|
node: { style },
|
|
@@ -5,12 +5,13 @@ import type {
|
|
|
5
5
|
EditorMentionTheme,
|
|
6
6
|
} from './EditorTheme';
|
|
7
7
|
import { normalizeEditorTheme } from './EditorStyleSheetNormalization';
|
|
8
|
+
|
|
8
9
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
9
10
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
function hasOnlyOwnKeys(value: Record<string, unknown>, fields: readonly string[]): boolean {
|
|
13
|
-
return Reflect.ownKeys(value).every(
|
|
14
|
+
return Reflect.ownKeys(value).every(key => typeof key === 'string' && fields.includes(key));
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export const MENTION_NODE_STRING_FIELDS = [
|
|
@@ -51,7 +52,17 @@ export const MENTION_SUGGESTIONS_NUMBER_FIELDS = [
|
|
|
51
52
|
|
|
52
53
|
export const EDITOR_MENTION_THEME_FONT_WEIGHTS: ReadonlySet<
|
|
53
54
|
NonNullable<EditorMentionNodeTheme['fontWeight']>
|
|
54
|
-
> = new Set(['normal',
|
|
55
|
+
> = new Set([ 'normal',
|
|
56
|
+
'bold',
|
|
57
|
+
'100',
|
|
58
|
+
'200',
|
|
59
|
+
'300',
|
|
60
|
+
'400',
|
|
61
|
+
'500',
|
|
62
|
+
'600',
|
|
63
|
+
'700',
|
|
64
|
+
'800',
|
|
65
|
+
'900' ]);
|
|
55
66
|
|
|
56
67
|
export const EDITOR_MENTION_THEME_FIELDS = [
|
|
57
68
|
'node',
|
|
@@ -74,14 +85,20 @@ export function validMentionThemeSection(
|
|
|
74
85
|
numberFields: readonly string[],
|
|
75
86
|
extraFields: readonly string[]
|
|
76
87
|
): value is Record<string, unknown> {
|
|
77
|
-
if (!isPlainRecord(value))
|
|
78
|
-
|
|
88
|
+
if (!isPlainRecord(value)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!hasOnlyOwnKeys(value, [ ...stringFields, ...numberFields, ...extraFields ])) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
return (
|
|
80
97
|
stringFields.every(
|
|
81
|
-
|
|
98
|
+
field => value[field] === undefined || typeof value[field] === 'string'
|
|
82
99
|
) &&
|
|
83
100
|
numberFields.every(
|
|
84
|
-
|
|
101
|
+
field =>
|
|
85
102
|
value[field] === undefined ||
|
|
86
103
|
(typeof value[field] === 'number' && Number.isFinite(value[field]))
|
|
87
104
|
)
|
|
@@ -94,40 +111,60 @@ export function validEditorMentionTheme(value: unknown): value is EditorMentionT
|
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
const { node, suggestions } = value;
|
|
114
|
+
|
|
97
115
|
if (node !== undefined) {
|
|
98
|
-
if (!isPlainRecord(node))
|
|
116
|
+
if (!isPlainRecord(node)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
99
120
|
const { textColor, style, ...appearance } = node;
|
|
100
|
-
|
|
121
|
+
|
|
122
|
+
if (textColor !== undefined && typeof textColor !== 'string') {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
101
126
|
try {
|
|
102
|
-
normalizeEditorTheme({ mention: [{ color: textColor }, appearance] });
|
|
127
|
+
normalizeEditorTheme({ mention: [ { color: textColor }, appearance ] });
|
|
128
|
+
|
|
103
129
|
if (style !== undefined) {
|
|
104
|
-
if (!isPlainRecord(style))
|
|
130
|
+
if (!isPlainRecord(style)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
105
134
|
normalizeEditorTheme({ mention: style });
|
|
106
135
|
}
|
|
107
136
|
} catch {
|
|
108
137
|
return false;
|
|
109
138
|
}
|
|
110
139
|
}
|
|
111
|
-
|
|
140
|
+
|
|
141
|
+
if (suggestions === undefined) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
112
145
|
if (
|
|
113
146
|
!validMentionThemeSection(
|
|
114
147
|
suggestions,
|
|
115
148
|
MENTION_SUGGESTIONS_STRING_FIELDS,
|
|
116
149
|
MENTION_SUGGESTIONS_NUMBER_FIELDS,
|
|
117
|
-
['option']
|
|
150
|
+
[ 'option' ]
|
|
118
151
|
)
|
|
119
152
|
) {
|
|
120
153
|
return false;
|
|
121
154
|
}
|
|
122
155
|
|
|
123
156
|
const option = suggestions.option;
|
|
124
|
-
|
|
157
|
+
|
|
158
|
+
if (option === undefined) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
125
162
|
return (
|
|
126
163
|
validMentionThemeSection(
|
|
127
164
|
option,
|
|
128
165
|
MENTION_OPTION_STRING_FIELDS,
|
|
129
166
|
MENTION_OPTION_NUMBER_FIELDS,
|
|
130
|
-
['fontWeight']
|
|
167
|
+
[ 'fontWeight' ]
|
|
131
168
|
) && validMentionFontWeight(option.fontWeight)
|
|
132
169
|
);
|
|
133
170
|
}
|
package/src/EditorStyleSheet.ts
CHANGED
|
@@ -17,27 +17,32 @@ const typography = [
|
|
|
17
17
|
'textDecorationColor',
|
|
18
18
|
'textDecorationStyle',
|
|
19
19
|
];
|
|
20
|
-
|
|
21
|
-
const
|
|
20
|
+
|
|
21
|
+
const sides = [ 'Top', 'Right', 'Bottom', 'Left' ];
|
|
22
|
+
const corners = [ 'TopLeft', 'TopRight', 'BottomRight', 'BottomLeft' ];
|
|
23
|
+
|
|
22
24
|
const border = [
|
|
23
25
|
'borderWidth',
|
|
24
26
|
'borderColor',
|
|
25
27
|
'borderStyle',
|
|
26
28
|
'borderRadius',
|
|
27
|
-
...sides.flatMap(
|
|
28
|
-
...corners.map(
|
|
29
|
+
...sides.flatMap(side => [ `border${side}Width`, `border${side}Color` ]),
|
|
30
|
+
...corners.map(corner => `border${corner}Radius`),
|
|
29
31
|
];
|
|
32
|
+
|
|
30
33
|
const spacing = (prefix: string) => [
|
|
31
34
|
prefix,
|
|
32
35
|
`${prefix}Horizontal`,
|
|
33
36
|
`${prefix}Vertical`,
|
|
34
|
-
...sides.map(
|
|
37
|
+
...sides.map(side => `${prefix}${side}`),
|
|
35
38
|
];
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
39
|
+
|
|
40
|
+
const surface = [ 'backgroundColor', ...border, ...spacing('padding') ];
|
|
41
|
+
const box = [ ...surface, ...spacing('margin') ];
|
|
42
|
+
const block = [ ...typography, ...box, 'textAlign' ];
|
|
43
|
+
const inline = [ ...typography, 'backgroundColor' ];
|
|
44
|
+
const checkbox = [ ...border, 'backgroundColor', 'checkColor' ];
|
|
45
|
+
|
|
41
46
|
const fields: Record<keyof EditorStyleMap, readonly string[]> = {
|
|
42
47
|
content: surface,
|
|
43
48
|
text: typography,
|
|
@@ -50,32 +55,47 @@ const fields: Record<keyof EditorStyleMap, readonly string[]> = {
|
|
|
50
55
|
h6: block,
|
|
51
56
|
blockquote: block,
|
|
52
57
|
codeBlock: block,
|
|
53
|
-
bulletList: [...block, 'indent', 'baseIndentMultiplier'],
|
|
54
|
-
orderedList: [...block, 'indent', 'baseIndentMultiplier'],
|
|
55
|
-
taskList: [...block, 'indent', 'baseIndentMultiplier'],
|
|
58
|
+
bulletList: [ ...block, 'indent', 'baseIndentMultiplier' ],
|
|
59
|
+
orderedList: [ ...block, 'indent', 'baseIndentMultiplier' ],
|
|
60
|
+
taskList: [ ...block, 'indent', 'baseIndentMultiplier' ],
|
|
56
61
|
listItem: block,
|
|
57
62
|
taskItem: block,
|
|
58
|
-
listMarker: ['color', 'scale', 'gap', 'ordered'],
|
|
59
|
-
taskCheckbox: [...checkbox, 'size', 'gap', 'checked'],
|
|
60
|
-
horizontalRule: [...border, ...spacing('margin'), 'backgroundColor', 'height'],
|
|
61
|
-
image: [...box, 'resizeMode'],
|
|
63
|
+
listMarker: [ 'color', 'scale', 'gap', 'ordered' ],
|
|
64
|
+
taskCheckbox: [ ...checkbox, 'size', 'gap', 'checked' ],
|
|
65
|
+
horizontalRule: [ ...border, ...spacing('margin'), 'backgroundColor', 'height' ],
|
|
66
|
+
image: [ ...box, 'resizeMode' ],
|
|
62
67
|
link: inline,
|
|
63
68
|
inlineCode: inline,
|
|
64
69
|
bold: inline,
|
|
65
70
|
italic: inline,
|
|
66
71
|
underline: inline,
|
|
67
72
|
strike: inline,
|
|
68
|
-
mention: [...inline, ...border],
|
|
73
|
+
mention: [ ...inline, ...border, ...spacing('padding') ],
|
|
69
74
|
placeholder: typography,
|
|
70
75
|
};
|
|
76
|
+
|
|
71
77
|
const enums: Record<string, readonly string[]> = {
|
|
72
|
-
fontStyle: ['normal', 'italic'],
|
|
73
|
-
fontWeight: ['normal',
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
fontStyle: [ 'normal', 'italic' ],
|
|
79
|
+
fontWeight: [ 'normal',
|
|
80
|
+
'bold',
|
|
81
|
+
'100',
|
|
82
|
+
'200',
|
|
83
|
+
'300',
|
|
84
|
+
'400',
|
|
85
|
+
'500',
|
|
86
|
+
'600',
|
|
87
|
+
'700',
|
|
88
|
+
'800',
|
|
89
|
+
'900' ],
|
|
90
|
+
textAlign: [ 'auto',
|
|
91
|
+
'left',
|
|
92
|
+
'right',
|
|
93
|
+
'center',
|
|
94
|
+
'justify' ],
|
|
95
|
+
textDecorationLine: [ 'none', 'underline', 'line-through', 'underline line-through' ],
|
|
96
|
+
textDecorationStyle: [ 'solid', 'double', 'dotted', 'dashed' ],
|
|
97
|
+
borderStyle: [ 'solid', 'dashed', 'dotted' ],
|
|
98
|
+
resizeMode: [ 'contain', 'cover', 'stretch' ],
|
|
79
99
|
};
|
|
80
100
|
|
|
81
101
|
function invalid(path: string, reason: string): never {
|
|
@@ -83,66 +103,122 @@ function invalid(path: string, reason: string): never {
|
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
function record(value: unknown): value is Record<string, unknown> {
|
|
86
|
-
if (value == null || typeof value !== 'object' || Array.isArray(value))
|
|
106
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
87
110
|
const prototype = Object.getPrototypeOf(value);
|
|
111
|
+
|
|
88
112
|
return prototype === Object.prototype || prototype === null;
|
|
89
113
|
}
|
|
90
114
|
|
|
91
115
|
export function normalizeEditorColor(value: unknown, path: string): string {
|
|
92
|
-
if (typeof value !== 'string')
|
|
116
|
+
if (typeof value !== 'string') {
|
|
117
|
+
return invalid(path, 'expected a color string');
|
|
118
|
+
}
|
|
119
|
+
|
|
93
120
|
const argb = processColor(value);
|
|
94
|
-
|
|
121
|
+
|
|
122
|
+
if (typeof argb !== 'number') {
|
|
123
|
+
return invalid(path, 'unrecognized color');
|
|
124
|
+
}
|
|
125
|
+
|
|
95
126
|
const rgba = ((argb << 8) | (argb >>> 24)) >>> 0;
|
|
127
|
+
|
|
96
128
|
return `#${rgba.toString(16).padStart(8, '0')}`;
|
|
97
129
|
}
|
|
98
130
|
|
|
99
131
|
function orderedMarker(value: unknown, path: string): Record<string, unknown> {
|
|
100
|
-
if (!record(value))
|
|
132
|
+
if (!record(value)) {
|
|
133
|
+
return invalid(path, 'expected an ordered marker object');
|
|
134
|
+
}
|
|
135
|
+
|
|
101
136
|
const result: Record<string, unknown> = {};
|
|
102
|
-
|
|
103
|
-
|
|
137
|
+
|
|
138
|
+
for (const [ key, entry ] of Object.entries(value)) {
|
|
139
|
+
if (key !== 'schemes' && key !== 'suffix') {
|
|
104
140
|
invalid(`${path}.${key}`, 'unsupported property');
|
|
105
|
-
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (entry === undefined) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
106
147
|
if (key === 'suffix') {
|
|
107
|
-
if (entry !== '.' && entry !== ')')
|
|
148
|
+
if (entry !== '.' && entry !== ')') {
|
|
149
|
+
invalid(`${path}.${key}`, 'expected . or )');
|
|
150
|
+
}
|
|
151
|
+
|
|
108
152
|
result[key] = entry;
|
|
109
153
|
} else {
|
|
110
|
-
const schemes = ['decimal',
|
|
154
|
+
const schemes = [ 'decimal',
|
|
155
|
+
'lowerAlpha',
|
|
156
|
+
'upperAlpha',
|
|
157
|
+
'lowerRoman',
|
|
158
|
+
'upperRoman' ];
|
|
159
|
+
|
|
111
160
|
if (
|
|
112
161
|
!Array.isArray(entry) ||
|
|
113
162
|
entry.length === 0 ||
|
|
114
|
-
!entry.every(
|
|
163
|
+
!entry.every(item => typeof item === 'string' && schemes.includes(item))
|
|
115
164
|
) {
|
|
116
165
|
invalid(`${path}.${key}`, 'expected a nonempty array of numbering schemes');
|
|
117
166
|
}
|
|
118
|
-
|
|
167
|
+
|
|
168
|
+
result[key] = [ ...entry ];
|
|
119
169
|
}
|
|
120
170
|
}
|
|
171
|
+
|
|
121
172
|
return result;
|
|
122
173
|
}
|
|
123
174
|
|
|
124
175
|
function property(key: string, value: unknown, path: string): unknown {
|
|
125
|
-
if (value === undefined)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
176
|
+
if (value === undefined) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (key === 'ordered') {
|
|
181
|
+
return orderedMarker(value, path);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (key === 'checked') {
|
|
185
|
+
return normalizeStyle(value, checkbox, path);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (key === 'color' || key.endsWith('Color')) {
|
|
189
|
+
return normalizeEditorColor(value, path);
|
|
190
|
+
}
|
|
191
|
+
|
|
129
192
|
if (key === 'fontFamily') {
|
|
130
|
-
if (typeof value !== 'string' || value.length === 0)
|
|
193
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
131
194
|
invalid(path, 'expected a nonempty font family');
|
|
195
|
+
}
|
|
196
|
+
|
|
132
197
|
return value;
|
|
133
198
|
}
|
|
199
|
+
|
|
134
200
|
if (enums[key]) {
|
|
135
201
|
const candidate = key === 'fontWeight' && typeof value === 'number' ? String(value) : value;
|
|
136
|
-
|
|
202
|
+
|
|
203
|
+
if (typeof candidate !== 'string' || !enums[key].includes(candidate)) {
|
|
137
204
|
invalid(path, 'unsupported value');
|
|
205
|
+
}
|
|
206
|
+
|
|
138
207
|
return candidate;
|
|
139
208
|
}
|
|
140
|
-
|
|
209
|
+
|
|
210
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
141
211
|
invalid(path, 'expected a finite number');
|
|
142
|
-
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if ([ 'fontSize', 'lineHeight', 'size', 'scale' ].includes(key) && value <= 0) {
|
|
143
215
|
invalid(path, 'expected a positive number');
|
|
144
|
-
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!key.startsWith('margin') && key !== 'letterSpacing' && value < 0) {
|
|
145
219
|
invalid(path, 'expected a nonnegative number');
|
|
220
|
+
}
|
|
221
|
+
|
|
146
222
|
return value;
|
|
147
223
|
}
|
|
148
224
|
|
|
@@ -153,67 +229,122 @@ function normalizeStyle(
|
|
|
153
229
|
): Record<string, unknown> {
|
|
154
230
|
const raw: Record<string, unknown> = {};
|
|
155
231
|
const ancestors = new Set<object>();
|
|
232
|
+
|
|
156
233
|
function append(entry: unknown, depth: number): void {
|
|
157
|
-
if (entry === false || entry == null)
|
|
158
|
-
|
|
234
|
+
if (entry === false || entry == null) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (depth > 64) {
|
|
239
|
+
invalid(path, 'style arrays are nested too deeply');
|
|
240
|
+
}
|
|
241
|
+
|
|
159
242
|
if (Array.isArray(entry)) {
|
|
160
|
-
if (ancestors.has(entry))
|
|
243
|
+
if (ancestors.has(entry)) {
|
|
244
|
+
invalid(path, 'cyclic style array');
|
|
245
|
+
}
|
|
246
|
+
|
|
161
247
|
ancestors.add(entry);
|
|
162
|
-
entry.forEach(
|
|
248
|
+
entry.forEach(item => append(item, depth + 1));
|
|
163
249
|
ancestors.delete(entry);
|
|
250
|
+
|
|
164
251
|
return;
|
|
165
252
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
253
|
+
|
|
254
|
+
if (!record(entry)) {
|
|
255
|
+
invalid(path, 'expected a style object or array');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
for (const [ key, field ] of Object.entries(entry)) {
|
|
259
|
+
if (!allowed.includes(key)) {
|
|
260
|
+
invalid(`${path}.${key}`, 'unsupported property');
|
|
261
|
+
}
|
|
262
|
+
|
|
169
263
|
raw[key] = property(key, field, `${path}.${key}`);
|
|
170
264
|
}
|
|
171
265
|
}
|
|
266
|
+
|
|
172
267
|
append(value, 0);
|
|
268
|
+
|
|
173
269
|
const result = Object.fromEntries(
|
|
174
|
-
Object.entries(raw).filter(([, value]) => value !== undefined)
|
|
270
|
+
Object.entries(raw).filter(([ , value ]) => value !== undefined)
|
|
175
271
|
);
|
|
176
|
-
|
|
272
|
+
|
|
273
|
+
for (const prefix of [ 'margin', 'padding' ]) {
|
|
177
274
|
for (const side of sides) {
|
|
178
275
|
const axis = side === 'Top' || side === 'Bottom' ? 'Vertical' : 'Horizontal';
|
|
179
276
|
const resolved = raw[`${prefix}${side}`] ?? raw[`${prefix}${axis}`] ?? raw[prefix];
|
|
180
|
-
|
|
277
|
+
|
|
278
|
+
if (resolved !== undefined) {
|
|
279
|
+
result[`${prefix}${side}`] = resolved;
|
|
280
|
+
}
|
|
181
281
|
}
|
|
282
|
+
|
|
182
283
|
delete result[prefix];
|
|
183
284
|
delete result[`${prefix}Horizontal`];
|
|
184
285
|
delete result[`${prefix}Vertical`];
|
|
185
286
|
}
|
|
186
|
-
|
|
287
|
+
|
|
288
|
+
for (const suffix of [ 'Width', 'Color' ]) {
|
|
187
289
|
for (const side of sides) {
|
|
188
290
|
const resolved = raw[`border${side}${suffix}`] ?? raw[`border${suffix}`];
|
|
189
|
-
|
|
291
|
+
|
|
292
|
+
if (resolved !== undefined) {
|
|
293
|
+
result[`border${side}${suffix}`] = resolved;
|
|
294
|
+
}
|
|
190
295
|
}
|
|
296
|
+
|
|
191
297
|
delete result[`border${suffix}`];
|
|
192
298
|
}
|
|
299
|
+
|
|
193
300
|
for (const corner of corners) {
|
|
194
301
|
const resolved = raw[`border${corner}Radius`] ?? raw.borderRadius;
|
|
195
|
-
|
|
302
|
+
|
|
303
|
+
if (resolved !== undefined) {
|
|
304
|
+
result[`border${corner}Radius`] = resolved;
|
|
305
|
+
}
|
|
196
306
|
}
|
|
307
|
+
|
|
197
308
|
delete result.borderRadius;
|
|
309
|
+
|
|
198
310
|
return result;
|
|
199
311
|
}
|
|
200
312
|
|
|
201
313
|
export function normalizeEditorTheme(theme: unknown): NormalizedEditorTheme {
|
|
202
|
-
if (!record(theme))
|
|
314
|
+
if (!record(theme)) {
|
|
315
|
+
invalid('theme', 'expected a named style map');
|
|
316
|
+
}
|
|
317
|
+
|
|
203
318
|
const styles: Partial<Record<keyof EditorStyleMap, NormalizedEditorStyle>> = {};
|
|
204
319
|
const result: NormalizedEditorTheme = { version: 1 };
|
|
205
|
-
|
|
320
|
+
|
|
321
|
+
for (const [ key, value ] of Object.entries(theme)) {
|
|
206
322
|
if (key === 'toolbar') {
|
|
207
323
|
if (value !== undefined) {
|
|
208
|
-
if (!record(value))
|
|
324
|
+
if (!record(value)) {
|
|
325
|
+
invalid('toolbar', 'expected toolbar configuration');
|
|
326
|
+
}
|
|
327
|
+
|
|
209
328
|
result.toolbar = { ...value };
|
|
210
329
|
}
|
|
330
|
+
|
|
211
331
|
continue;
|
|
212
332
|
}
|
|
213
|
-
|
|
333
|
+
|
|
334
|
+
if (!Object.prototype.hasOwnProperty.call(fields, key)) {
|
|
335
|
+
invalid(key, 'unsupported element');
|
|
336
|
+
}
|
|
337
|
+
|
|
214
338
|
const style = normalizeStyle(value, fields[key as keyof EditorStyleMap], key);
|
|
215
|
-
|
|
339
|
+
|
|
340
|
+
if (Object.keys(style).length) {
|
|
341
|
+
styles[key as keyof EditorStyleMap] = style;
|
|
342
|
+
}
|
|
216
343
|
}
|
|
217
|
-
|
|
344
|
+
|
|
345
|
+
if (Object.keys(styles).length) {
|
|
346
|
+
result.styles = styles;
|
|
347
|
+
}
|
|
348
|
+
|
|
218
349
|
return result;
|
|
219
350
|
}
|
|
@@ -113,7 +113,7 @@ export interface EditorHorizontalRuleStyle extends EditorBorderStyle, EditorMarg
|
|
|
113
113
|
height?: number;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
export interface EditorMentionStyle extends EditorInlineStyle, EditorBorderStyle {}
|
|
116
|
+
export interface EditorMentionStyle extends EditorInlineStyle, EditorBorderStyle, EditorPaddingStyle {}
|
|
117
117
|
|
|
118
118
|
export interface EditorStyleMap {
|
|
119
119
|
content: EditorSurfaceStyle;
|