@riducms/plugin-richtext 0.2.3 → 0.3.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/README.md +7 -1
- package/package.json +7 -5
- package/src/block/rich-text-block-card.scss +139 -0
- package/src/block/rich-text-block-card.svelte +42 -21
- package/src/block/rich-text-block-clipboard.ts +1 -1
- package/src/block/rich-text-block-node.ts +1 -1
- package/src/block/rich-text-blocks-plugin.svelte +5 -2
- package/src/document-validation.ts +7 -5
- package/src/field/reference-node-placement.ts +49 -0
- package/src/field/rich-text-blocks.ts +1 -5
- package/src/field/rich-text-config.ts +1 -4
- package/src/field/rich-text-document.ts +1 -4
- package/src/field/rich-text-editor.svelte +105 -57
- package/src/field/rich-text-footer.svelte +7 -12
- package/src/field/rich-text-markdown.ts +62 -0
- package/src/link/link-url.ts +30 -0
- package/src/link/rich-text-link-plugin.svelte +528 -0
- package/src/link/rich-text-link.scss +144 -0
- package/src/link/safe-link-transform.ts +20 -0
- package/src/link-url-policy.ts +14 -0
- package/src/menu/rich-text-block-toolbar.svelte +403 -70
- package/src/menu/rich-text-commands.ts +10 -1
- package/src/menu/rich-text-menu.scss +92 -0
- package/src/menu/rich-text-menu.svelte +62 -69
- package/src/menu/rich-text-option-filter.ts +30 -0
- package/src/menu/rich-text-options.ts +81 -84
- package/src/menu/rich-text-slash-menu.svelte +15 -11
- package/src/messages.ts +72 -27
- package/src/relationship/rich-text-relationship-node.scss +98 -0
- package/src/relationship/rich-text-relationship-node.svelte +38 -32
- package/src/relationship/rich-text-relationship-plugin.svelte +24 -40
- package/src/render/rich-text.svelte +10 -5
- package/src/render.ts +3 -8
- package/src/styles/integration.scss +384 -0
- package/src/toolbar/PAYLOAD-LICENSE.md +22 -0
- package/src/toolbar/rich-text-floating-toolbar.svelte +331 -169
- package/src/toolbar/rich-text-toolbar.scss +80 -0
- package/src/toolbar/toolbar-icon.svelte +200 -30
- package/src/upload/rich-text-upload-node.scss +308 -0
- package/src/upload/rich-text-upload-node.svelte +96 -125
- package/src/upload/rich-text-upload-node.ts +1 -0
- package/src/upload/rich-text-upload-plugin.svelte +10 -33
- package/src/styles/integration.css +0 -83
|
@@ -6,24 +6,40 @@
|
|
|
6
6
|
import { CodeNode } from "@lexical/code";
|
|
7
7
|
import { HistoryExtension } from "@lexical/history";
|
|
8
8
|
import { LinkNode } from "@lexical/link";
|
|
9
|
-
import { ListExtension } from "@lexical/list";
|
|
9
|
+
import { CheckListExtension, ListExtension } from "@lexical/list";
|
|
10
|
+
import { registerMarkdownShortcuts } from "@lexical/markdown";
|
|
10
11
|
import { RichTextExtension } from "@lexical/rich-text";
|
|
12
|
+
import { $insertNodeToNearestRoot } from "@lexical/utils";
|
|
11
13
|
import {
|
|
12
14
|
ContentEditable,
|
|
15
|
+
DEFAULT_TRANSFORMERS,
|
|
16
|
+
$createHorizontalRuleNode,
|
|
13
17
|
HorizontalRuleNode,
|
|
14
|
-
|
|
18
|
+
INSERT_HORIZONTAL_RULE_COMMAND,
|
|
15
19
|
LexicalExtensionComposer,
|
|
16
20
|
LinkPlugin,
|
|
17
21
|
OnChangePlugin,
|
|
18
22
|
} from "@hvniel/lexical-svelte";
|
|
19
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
$createParagraphNode,
|
|
25
|
+
$getRoot,
|
|
26
|
+
$getSelection,
|
|
27
|
+
$isRangeSelection,
|
|
28
|
+
COMMAND_PRIORITY_EDITOR,
|
|
29
|
+
defineExtension,
|
|
30
|
+
mergeRegister,
|
|
31
|
+
type EditorState,
|
|
32
|
+
} from "lexical";
|
|
20
33
|
import { Button, FieldFrame, fieldControlARIA } from "@riducms/ui";
|
|
21
34
|
|
|
22
|
-
import "@plugin-richtext/styles/integration.
|
|
35
|
+
import "@plugin-richtext/styles/integration.scss";
|
|
23
36
|
import RichTextBlockToolbar from "@plugin-richtext/menu/rich-text-block-toolbar.svelte";
|
|
24
37
|
import RichTextEditabilityPlugin from "@plugin-richtext/field/rich-text-editability-plugin.svelte";
|
|
25
38
|
import RichTextFooter from "@plugin-richtext/field/rich-text-footer.svelte";
|
|
26
39
|
import RichTextFloatingToolbar from "@plugin-richtext/toolbar/rich-text-floating-toolbar.svelte";
|
|
40
|
+
import RichTextLinkPlugin from "@plugin-richtext/link/rich-text-link-plugin.svelte";
|
|
41
|
+
import { registerSafeLinkTransform } from "@plugin-richtext/link/safe-link-transform";
|
|
42
|
+
import { richTextMarkdownTransformers } from "@plugin-richtext/field/rich-text-markdown";
|
|
27
43
|
import RichTextSlashMenu from "@plugin-richtext/menu/rich-text-slash-menu.svelte";
|
|
28
44
|
import { hasRichTextFeature } from "@plugin-richtext/field/rich-text-config";
|
|
29
45
|
import {
|
|
@@ -48,15 +64,14 @@
|
|
|
48
64
|
authoring,
|
|
49
65
|
i18n,
|
|
50
66
|
}: PluginFieldProps<RichTextDocument<unknown>, RichTextConfig> = $props();
|
|
51
|
-
const field = $derived(binding
|
|
52
|
-
const editingBlocked = $derived(binding.readOnly);
|
|
53
|
-
const issues = $derived(binding.issues);
|
|
67
|
+
const { schema: field, readOnly: editingBlocked, issues } = $derived(binding);
|
|
54
68
|
const inputARIA = $derived(
|
|
55
69
|
fieldControlARIA(field.id, field.admin.description !== undefined, issues.length > 0)
|
|
56
70
|
);
|
|
57
|
-
const placeholderText = $derived(i18n.t("plugin.richtext:editor.placeholder"));
|
|
58
71
|
const visiblePlaceholder = $derived(
|
|
59
|
-
|
|
72
|
+
editingBlocked
|
|
73
|
+
? i18n.t("plugin.richtext:editor.empty")
|
|
74
|
+
: i18n.t("plugin.richtext:editor.placeholder")
|
|
60
75
|
);
|
|
61
76
|
let canvasElement = $state<HTMLElement | null>(null);
|
|
62
77
|
// A mounted field hydrates once; the parent remounts it for schema/revision/locale changes.
|
|
@@ -79,38 +94,45 @@
|
|
|
79
94
|
|
|
80
95
|
const theme = {
|
|
81
96
|
heading: {
|
|
82
|
-
h1: "ridu-richtext-heading ridu-richtext-h1
|
|
83
|
-
h2: "ridu-richtext-heading ridu-richtext-h2
|
|
84
|
-
h3: "ridu-richtext-heading ridu-richtext-h3
|
|
97
|
+
h1: "ridu-richtext-heading ridu-richtext-h1",
|
|
98
|
+
h2: "ridu-richtext-heading ridu-richtext-h2",
|
|
99
|
+
h3: "ridu-richtext-heading ridu-richtext-h3",
|
|
100
|
+
h4: "ridu-richtext-heading ridu-richtext-h4",
|
|
101
|
+
h5: "ridu-richtext-heading ridu-richtext-h5",
|
|
102
|
+
h6: "ridu-richtext-heading ridu-richtext-h6",
|
|
85
103
|
},
|
|
86
|
-
code: "ridu-richtext-code-block
|
|
87
|
-
hr: "ridu-richtext-rule
|
|
104
|
+
code: "ridu-richtext-code-block",
|
|
105
|
+
hr: "ridu-richtext-rule",
|
|
88
106
|
hrSelected: "is-selected",
|
|
89
|
-
link: "ridu-richtext-link
|
|
107
|
+
link: "ridu-richtext-link",
|
|
90
108
|
list: {
|
|
91
|
-
checklist:
|
|
92
|
-
|
|
93
|
-
listitemChecked: "ridu-richtext-list-item-checked line-through opacity-70",
|
|
109
|
+
checklist: "ridu-richtext-checklist",
|
|
110
|
+
listitemChecked: "ridu-richtext-list-item-checked",
|
|
94
111
|
listitemUnchecked: "ridu-richtext-list-item-unchecked",
|
|
95
|
-
listitem: "ridu-richtext-list-item
|
|
112
|
+
listitem: "ridu-richtext-list-item",
|
|
113
|
+
olDepth: [
|
|
114
|
+
"ridu-richtext-list-ordered",
|
|
115
|
+
"ridu-richtext-list-alpha-upper",
|
|
116
|
+
"ridu-richtext-list-alpha-lower",
|
|
117
|
+
"ridu-richtext-list-roman-upper",
|
|
118
|
+
"ridu-richtext-list-roman-lower",
|
|
119
|
+
],
|
|
96
120
|
nested: { listitem: "ridu-richtext-list-item-nested" },
|
|
97
|
-
ol: "ridu-richtext-list-ordered
|
|
98
|
-
ul: "ridu-richtext-list-unordered
|
|
121
|
+
ol: "ridu-richtext-list-ordered",
|
|
122
|
+
ul: "ridu-richtext-list-unordered",
|
|
99
123
|
},
|
|
100
|
-
paragraph:
|
|
101
|
-
|
|
102
|
-
quote:
|
|
103
|
-
"ridu-richtext-quote my-[1.85rem] border-s-2 border-primary/55 py-[0.1rem] ps-5 font-serif text-[19px] italic leading-6 text-foreground-strong text-pretty",
|
|
124
|
+
paragraph: "ridu-richtext-paragraph",
|
|
125
|
+
quote: "ridu-richtext-quote",
|
|
104
126
|
text: {
|
|
105
|
-
bold: "ridu-richtext-bold
|
|
106
|
-
code: "ridu-richtext-inline-code
|
|
107
|
-
italic: "ridu-richtext-italic
|
|
127
|
+
bold: "ridu-richtext-bold",
|
|
128
|
+
code: "ridu-richtext-inline-code",
|
|
129
|
+
italic: "ridu-richtext-italic",
|
|
108
130
|
subscript: "ridu-richtext-subscript",
|
|
109
131
|
superscript: "ridu-richtext-superscript",
|
|
110
|
-
strikethrough: "ridu-richtext-strikethrough
|
|
111
|
-
underline: "ridu-richtext-underline
|
|
132
|
+
strikethrough: "ridu-richtext-strikethrough",
|
|
133
|
+
underline: "ridu-richtext-underline",
|
|
112
134
|
},
|
|
113
|
-
upload: "ridu-richtext-upload
|
|
135
|
+
upload: "ridu-richtext-upload",
|
|
114
136
|
};
|
|
115
137
|
|
|
116
138
|
// The extension tree is identity-bearing and belongs to this field instance.
|
|
@@ -125,8 +147,38 @@
|
|
|
125
147
|
dependencies: [
|
|
126
148
|
RichTextExtension,
|
|
127
149
|
HistoryExtension,
|
|
128
|
-
...(hasRichTextFeature(config, "lists") ? [ListExtension] : []),
|
|
150
|
+
...(hasRichTextFeature(config, "lists") ? [ListExtension, CheckListExtension] : []),
|
|
129
151
|
],
|
|
152
|
+
register: (editor) =>
|
|
153
|
+
mergeRegister(
|
|
154
|
+
registerMarkdownShortcuts(editor, [
|
|
155
|
+
...richTextMarkdownTransformers(config),
|
|
156
|
+
...(hasRichTextFeature(config, "horizontal-rule")
|
|
157
|
+
? DEFAULT_TRANSFORMERS.filter(
|
|
158
|
+
(transformer) =>
|
|
159
|
+
transformer.type === "element" &&
|
|
160
|
+
transformer.dependencies.includes(HorizontalRuleNode)
|
|
161
|
+
)
|
|
162
|
+
: []),
|
|
163
|
+
]),
|
|
164
|
+
// The upstream horizontal-rule plugin treats a cross-block drag's root click
|
|
165
|
+
// as a gap click and collapses the range; Lexical handles gap clicks itself.
|
|
166
|
+
...(hasRichTextFeature(config, "horizontal-rule")
|
|
167
|
+
? [
|
|
168
|
+
editor.registerCommand(
|
|
169
|
+
INSERT_HORIZONTAL_RULE_COMMAND,
|
|
170
|
+
() => {
|
|
171
|
+
const selection = $getSelection();
|
|
172
|
+
if (!$isRangeSelection(selection)) return false;
|
|
173
|
+
$insertNodeToNearestRoot($createHorizontalRuleNode());
|
|
174
|
+
return true;
|
|
175
|
+
},
|
|
176
|
+
COMMAND_PRIORITY_EDITOR
|
|
177
|
+
),
|
|
178
|
+
]
|
|
179
|
+
: []),
|
|
180
|
+
...(hasRichTextFeature(config, "links") ? [registerSafeLinkTransform(editor)] : [])
|
|
181
|
+
),
|
|
130
182
|
name: "@riducms/plugin-richtext/editor",
|
|
131
183
|
namespace: `ridu-${field.id}`,
|
|
132
184
|
nodes: [
|
|
@@ -153,6 +205,7 @@
|
|
|
153
205
|
)
|
|
154
206
|
);
|
|
155
207
|
}
|
|
208
|
+
|
|
156
209
|
function exportDocument() {
|
|
157
210
|
const url = URL.createObjectURL(
|
|
158
211
|
new Blob([JSON.stringify(initialValue, null, 2)], { type: "application/json" })
|
|
@@ -173,20 +226,18 @@
|
|
|
173
226
|
readOnly={field.admin.readOnly}
|
|
174
227
|
description={field.admin.description}
|
|
175
228
|
errors={issues.map((issue) => issue.message)}
|
|
176
|
-
class="
|
|
229
|
+
class="ridu-richtext-field"
|
|
177
230
|
>
|
|
178
231
|
<div
|
|
179
232
|
class={[
|
|
180
|
-
"ridu-richtext-editor
|
|
181
|
-
issues.length > 0 && "border-b-destructive/22",
|
|
182
|
-
field.admin.readOnly && "pt-[1.6rem]",
|
|
233
|
+
"ridu-richtext-editor",
|
|
183
234
|
issues.length > 0 && "has-error",
|
|
184
|
-
|
|
235
|
+
editingBlocked && "is-read-only",
|
|
185
236
|
]}
|
|
186
237
|
aria-invalid={inputARIA["aria-invalid"]}
|
|
187
238
|
>
|
|
188
239
|
{#if recoveryIssue !== undefined}
|
|
189
|
-
<p role="alert" class="
|
|
240
|
+
<p role="alert" class="ridu-richtext-recovery">
|
|
190
241
|
{i18n.t("plugin.richtext:editor.recovery", { path: `${field.path}.${recoveryIssue}` })}
|
|
191
242
|
</p>
|
|
192
243
|
<Button variant="outline" onclick={exportDocument}>
|
|
@@ -194,41 +245,38 @@
|
|
|
194
245
|
</Button>
|
|
195
246
|
{:else}
|
|
196
247
|
<LexicalExtensionComposer {extension} contentEditable={null}>
|
|
197
|
-
<div class="
|
|
248
|
+
<div class="ridu-richtext-canvas" bind:this={canvasElement}>
|
|
198
249
|
<ContentEditable
|
|
199
250
|
id={field.id}
|
|
200
|
-
class=
|
|
201
|
-
"ridu-richtext-content relative z-1 min-h-44 pt-[1.35rem] pe-0 pb-3 ps-9 text-foreground-body caret-primary outline-none focus-visible:outline-none max-[34rem]:min-h-36 max-[34rem]:pt-4 max-[34rem]:ps-8",
|
|
202
|
-
field.admin.readOnly
|
|
203
|
-
? "min-h-auto pt-0 ps-0 max-[34rem]:min-h-auto max-[34rem]:pt-0 max-[34rem]:ps-0"
|
|
204
|
-
: "border-s border-border transition-colors duration-150",
|
|
205
|
-
issues.length > 0 && !field.admin.readOnly && "border-s-destructive/72",
|
|
206
|
-
]}
|
|
251
|
+
class="ridu-richtext-content"
|
|
207
252
|
ariaLabel={field.admin.label}
|
|
208
253
|
ariaDescribedBy={inputARIA["aria-describedby"]}
|
|
209
254
|
ariaErrorMessage={inputARIA["aria-errormessage"]}
|
|
210
255
|
ariaInvalid={inputARIA["aria-invalid"]}
|
|
211
256
|
ariaRequired={field.required}
|
|
212
|
-
aria-keyshortcuts=
|
|
257
|
+
aria-keyshortcuts={editingBlocked
|
|
258
|
+
? undefined
|
|
259
|
+
: hasRichTextFeature(config, "links")
|
|
260
|
+
? "Alt+Shift+ArrowUp Alt+Shift+ArrowDown Control+K Meta+K"
|
|
261
|
+
: "Alt+Shift+ArrowUp Alt+Shift+ArrowDown"}
|
|
213
262
|
aria-placeholder={visiblePlaceholder}
|
|
214
263
|
>
|
|
215
264
|
{#snippet placeholder()}
|
|
216
|
-
<span
|
|
217
|
-
class={[
|
|
218
|
-
"pointer-events-none absolute top-[1.35rem] start-9 z-0 text-[15.5px] leading-7 text-foreground-placeholder max-[34rem]:top-4 max-[34rem]:start-8",
|
|
219
|
-
field.admin.readOnly && "top-0 start-0 max-[34rem]:top-0 max-[34rem]:start-0",
|
|
220
|
-
]}
|
|
221
|
-
>
|
|
265
|
+
<span class="ridu-richtext-placeholder">
|
|
222
266
|
{visiblePlaceholder}
|
|
223
267
|
</span>
|
|
224
268
|
{/snippet}
|
|
225
269
|
</ContentEditable>
|
|
226
270
|
</div>
|
|
227
|
-
{#if hasRichTextFeature(config, "links")}
|
|
228
|
-
|
|
271
|
+
{#if hasRichTextFeature(config, "links")}
|
|
272
|
+
<LinkPlugin />
|
|
273
|
+
{/if}
|
|
229
274
|
{#if !editingBlocked}
|
|
230
275
|
<RichTextSlashMenu {authoring} {config} />
|
|
231
|
-
<RichTextFloatingToolbar />
|
|
276
|
+
<RichTextFloatingToolbar {config} />
|
|
277
|
+
{#if hasRichTextFeature(config, "links")}
|
|
278
|
+
<RichTextLinkPlugin />
|
|
279
|
+
{/if}
|
|
232
280
|
{#if canvasElement !== null}
|
|
233
281
|
<RichTextBlockToolbar anchorElement={canvasElement} {authoring} {config} />
|
|
234
282
|
{/if}
|
|
@@ -242,7 +290,7 @@
|
|
|
242
290
|
<RichTextEditabilityPlugin readOnly={editingBlocked} />
|
|
243
291
|
<RichTextBlocksPlugin {authoring} {field} />
|
|
244
292
|
<OnChangePlugin onChange={changed} ignoreSelectionChange />
|
|
245
|
-
<RichTextFooter readOnly={
|
|
293
|
+
<RichTextFooter readOnly={editingBlocked} />
|
|
246
294
|
</LexicalExtensionComposer>
|
|
247
295
|
{/if}
|
|
248
296
|
</div>
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getAdminI18n } from "@riducms/plugin";
|
|
3
3
|
import { useLexicalComposerContext, useLexicalEditable } from "@hvniel/lexical-svelte";
|
|
4
|
-
import {
|
|
5
|
-
$createParagraphNode,
|
|
6
|
-
$getRoot,
|
|
7
|
-
$getSelection,
|
|
8
|
-
$isParagraphNode,
|
|
9
|
-
$isRangeSelection,
|
|
10
|
-
} from "lexical";
|
|
4
|
+
import { $createParagraphNode, $getRoot, $isParagraphNode } from "lexical";
|
|
11
5
|
|
|
12
6
|
const editor = useLexicalComposerContext()[0];
|
|
13
7
|
const isEditable = useLexicalEditable();
|
|
14
8
|
const i18n = getAdminI18n();
|
|
9
|
+
const componentID = $props.id();
|
|
10
|
+
const hintID = `${componentID}-hint`;
|
|
15
11
|
let { readOnly = false }: { readOnly?: boolean } = $props();
|
|
16
12
|
|
|
17
13
|
function focusAtEnd() {
|
|
@@ -27,8 +23,6 @@
|
|
|
27
23
|
: $createParagraphNode();
|
|
28
24
|
if (paragraph.getParent() === null) root.append(paragraph);
|
|
29
25
|
paragraph.selectEnd();
|
|
30
|
-
const selection = $getSelection();
|
|
31
|
-
if ($isRangeSelection(selection)) selection.insertText("/");
|
|
32
26
|
});
|
|
33
27
|
},
|
|
34
28
|
{ defaultSelection: "rootEnd" }
|
|
@@ -38,14 +32,15 @@
|
|
|
38
32
|
|
|
39
33
|
{#if !readOnly}
|
|
40
34
|
<button
|
|
41
|
-
class="
|
|
35
|
+
class="ridu-richtext-footer"
|
|
42
36
|
type="button"
|
|
43
37
|
disabled={!isEditable()}
|
|
38
|
+
aria-label={i18n.t("plugin.richtext:editor.insertParagraph")}
|
|
39
|
+
aria-describedby={hintID}
|
|
44
40
|
onclick={focusAtEnd}
|
|
45
41
|
>
|
|
46
42
|
<span aria-hidden="true">+</span>
|
|
47
|
-
{
|
|
48
|
-
<span class="text-foreground-sub max-[34rem]:hidden">
|
|
43
|
+
<span id={hintID} class="ridu-richtext-announcement">
|
|
49
44
|
{i18n.t("plugin.richtext:editor.footerHint")}
|
|
50
45
|
</span>
|
|
51
46
|
</button>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BOLD_ITALIC_STAR,
|
|
3
|
+
BOLD_ITALIC_UNDERSCORE,
|
|
4
|
+
BOLD_STAR,
|
|
5
|
+
BOLD_UNDERSCORE,
|
|
6
|
+
CHECK_LIST,
|
|
7
|
+
CODE,
|
|
8
|
+
HEADING,
|
|
9
|
+
INLINE_CODE,
|
|
10
|
+
ITALIC_STAR,
|
|
11
|
+
ITALIC_UNDERSCORE,
|
|
12
|
+
LINK,
|
|
13
|
+
ORDERED_LIST,
|
|
14
|
+
QUOTE,
|
|
15
|
+
STRIKETHROUGH,
|
|
16
|
+
UNORDERED_LIST,
|
|
17
|
+
type TextMatchTransformer,
|
|
18
|
+
type Transformer,
|
|
19
|
+
} from "@lexical/markdown";
|
|
20
|
+
import { $isLinkNode } from "@lexical/link";
|
|
21
|
+
|
|
22
|
+
import { hasRichTextFeature, type RichTextConfig } from "@plugin-richtext/field/rich-text-config";
|
|
23
|
+
import { normalizeLinkURL } from "@plugin-richtext/link/link-url";
|
|
24
|
+
|
|
25
|
+
const safeLink: TextMatchTransformer = {
|
|
26
|
+
...LINK,
|
|
27
|
+
replace(textNode, match) {
|
|
28
|
+
// Match Lexical's CommonMark unescaping before validation. Pass the original
|
|
29
|
+
// match to its transformer so escaped text/title are still decoded only once.
|
|
30
|
+
const decoded = (match[2] ?? "")
|
|
31
|
+
.replace(/\\([!-/:-@[-`{-~])/g, "$1")
|
|
32
|
+
.replace(/&#(\d+);/g, (_, codePoint: string) => {
|
|
33
|
+
const value = Number(codePoint);
|
|
34
|
+
return value <= 0x10ffff ? String.fromCodePoint(value) : "\0";
|
|
35
|
+
});
|
|
36
|
+
const url = normalizeLinkURL(decoded);
|
|
37
|
+
if (url === undefined) return;
|
|
38
|
+
|
|
39
|
+
const replacement = LINK.replace?.(textNode, match);
|
|
40
|
+
const link = replacement?.getParent();
|
|
41
|
+
if ($isLinkNode(link)) link.setURL(url);
|
|
42
|
+
return replacement;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Shortcuts produce only nodes enabled by this field's canonical configuration. */
|
|
47
|
+
export function richTextMarkdownTransformers(config: RichTextConfig): Transformer[] {
|
|
48
|
+
return [
|
|
49
|
+
HEADING,
|
|
50
|
+
QUOTE,
|
|
51
|
+
...(hasRichTextFeature(config, "lists") ? [CHECK_LIST, UNORDERED_LIST, ORDERED_LIST] : []),
|
|
52
|
+
...(hasRichTextFeature(config, "code") ? [CODE, INLINE_CODE] : []),
|
|
53
|
+
BOLD_ITALIC_STAR,
|
|
54
|
+
BOLD_ITALIC_UNDERSCORE,
|
|
55
|
+
BOLD_STAR,
|
|
56
|
+
BOLD_UNDERSCORE,
|
|
57
|
+
ITALIC_STAR,
|
|
58
|
+
ITALIC_UNDERSCORE,
|
|
59
|
+
STRIKETHROUGH,
|
|
60
|
+
...(hasRichTextFeature(config, "links") ? [safeLink] : []),
|
|
61
|
+
];
|
|
62
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { formatUrl } from "@lexical/link";
|
|
2
|
+
import { renderableRichTextURL } from "@plugin-richtext/link-url-policy";
|
|
3
|
+
|
|
4
|
+
const relativeURL = /^(?:\/|\.|#)/;
|
|
5
|
+
|
|
6
|
+
/** Normalize an author-entered URL while rejecting schemes browsers could execute. */
|
|
7
|
+
export function normalizeLinkURL(value: string): string | undefined {
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
if (trimmed === "" || /[\u0000-\u001f\u007f\s]/.test(trimmed)) return undefined;
|
|
10
|
+
|
|
11
|
+
const normalized = formatUrl(trimmed);
|
|
12
|
+
const safeURL = renderableRichTextURL(normalized);
|
|
13
|
+
if (safeURL === undefined) return undefined;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const parsed = new URL(safeURL, "https://ridu.invalid");
|
|
17
|
+
if (
|
|
18
|
+
!relativeURL.test(safeURL) &&
|
|
19
|
+
(parsed.protocol === "http:" || parsed.protocol === "https:") &&
|
|
20
|
+
parsed.hostname === ""
|
|
21
|
+
)
|
|
22
|
+
return undefined;
|
|
23
|
+
if ((parsed.protocol === "mailto:" || parsed.protocol === "tel:") && parsed.pathname === "")
|
|
24
|
+
return undefined;
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return safeURL;
|
|
30
|
+
}
|