@blue-patterns/richtext 0.0.0-stage → 0.7.18
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/CHANGELOG.md +275 -0
- package/LICENSE +22 -0
- package/README.md +88 -2
- package/astryx.integration.mjs +11 -0
- package/blocks/RichTextEditorShowcase.template.mjs +13 -0
- package/blocks/RichTextEditorShowcase.tsx +19 -0
- package/dist/RichTextEditor.d.ts +282 -0
- package/dist/RichTextEditor.d.ts.map +1 -0
- package/dist/RichTextEditor.js +657 -0
- package/dist/RichTextEditorAutoLinkPlugin.d.ts +83 -0
- package/dist/RichTextEditorAutoLinkPlugin.d.ts.map +1 -0
- package/dist/RichTextEditorAutoLinkPlugin.js +99 -0
- package/dist/RichTextEditorToolbar.d.ts +186 -0
- package/dist/RichTextEditorToolbar.d.ts.map +1 -0
- package/dist/RichTextEditorToolbar.js +688 -0
- package/dist/RichTextView.d.ts +92 -0
- package/dist/RichTextView.d.ts.map +1 -0
- package/dist/RichTextView.js +267 -0
- package/dist/codeLanguages.d.ts +14 -0
- package/dist/codeLanguages.d.ts.map +1 -0
- package/dist/codeLanguages.js +34 -0
- package/dist/codeLineTokens.d.ts +53 -0
- package/dist/codeLineTokens.d.ts.map +1 -0
- package/dist/codeLineTokens.js +188 -0
- package/dist/editorNodes.d.ts +8 -0
- package/dist/editorNodes.d.ts.map +1 -0
- package/dist/editorNodes.js +30 -0
- package/dist/editorTheme.d.ts +7 -0
- package/dist/editorTheme.d.ts.map +1 -0
- package/dist/editorTheme.js +328 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/internal/CodeBackspacePlugin.d.ts +2 -0
- package/dist/internal/CodeBackspacePlugin.d.ts.map +1 -0
- package/dist/internal/CodeBackspacePlugin.js +70 -0
- package/dist/internal/CodeBlockHeaderPlugin.d.ts +11 -0
- package/dist/internal/CodeBlockHeaderPlugin.d.ts.map +1 -0
- package/dist/internal/CodeBlockHeaderPlugin.js +204 -0
- package/dist/internal/CodePastePlugin.d.ts +5 -0
- package/dist/internal/CodePastePlugin.d.ts.map +1 -0
- package/dist/internal/CodePastePlugin.js +104 -0
- package/dist/internal/CodeSyntaxPlugin.d.ts +8 -0
- package/dist/internal/CodeSyntaxPlugin.d.ts.map +1 -0
- package/dist/internal/CodeSyntaxPlugin.js +397 -0
- package/dist/internal/CodeTabPlugin.d.ts +2 -0
- package/dist/internal/CodeTabPlugin.d.ts.map +1 -0
- package/dist/internal/CodeTabPlugin.js +256 -0
- package/dist/internal/DisabledLinkGuardPlugin.d.ts +4 -0
- package/dist/internal/DisabledLinkGuardPlugin.d.ts.map +1 -0
- package/dist/internal/DisabledLinkGuardPlugin.js +93 -0
- package/dist/internal/ImageSourceCaretPlugin.d.ts +3 -0
- package/dist/internal/ImageSourceCaretPlugin.d.ts.map +1 -0
- package/dist/internal/ImageSourceCaretPlugin.js +105 -0
- package/dist/internal/ListItemShortcutsPlugin.d.ts +6 -0
- package/dist/internal/ListItemShortcutsPlugin.d.ts.map +1 -0
- package/dist/internal/ListItemShortcutsPlugin.js +314 -0
- package/dist/internal/MarkdownExtensionsPlugin.d.ts +17 -0
- package/dist/internal/MarkdownExtensionsPlugin.d.ts.map +1 -0
- package/dist/internal/MarkdownExtensionsPlugin.js +185 -0
- package/dist/internal/SelectAllScopePlugin.d.ts +2 -0
- package/dist/internal/SelectAllScopePlugin.d.ts.map +1 -0
- package/dist/internal/SelectAllScopePlugin.js +67 -0
- package/dist/internal/TableColumnFloorPlugin.d.ts +12 -0
- package/dist/internal/TableColumnFloorPlugin.d.ts.map +1 -0
- package/dist/internal/TableColumnFloorPlugin.js +84 -0
- package/dist/internal/TablePastePlugin.d.ts +9 -0
- package/dist/internal/TablePastePlugin.d.ts.map +1 -0
- package/dist/internal/TablePastePlugin.js +183 -0
- package/dist/internal/TableScrollRegionPlugin.d.ts +9 -0
- package/dist/internal/TableScrollRegionPlugin.d.ts.map +1 -0
- package/dist/internal/TableScrollRegionPlugin.js +95 -0
- package/dist/internal/TaskCheckboxPlugin.d.ts +7 -0
- package/dist/internal/TaskCheckboxPlugin.d.ts.map +1 -0
- package/dist/internal/TaskCheckboxPlugin.js +193 -0
- package/dist/internal/ToolbarLinkPopover.d.ts +74 -0
- package/dist/internal/ToolbarLinkPopover.d.ts.map +1 -0
- package/dist/internal/ToolbarLinkPopover.js +292 -0
- package/dist/linkEditing.d.ts +6 -0
- package/dist/linkEditing.d.ts.map +1 -0
- package/dist/linkEditing.js +20 -0
- package/dist/linkForm.d.ts +28 -0
- package/dist/linkForm.d.ts.map +1 -0
- package/dist/linkForm.js +137 -0
- package/dist/linkUtils.d.ts +27 -0
- package/dist/linkUtils.d.ts.map +1 -0
- package/dist/linkUtils.js +101 -0
- package/dist/markdown.d.ts +17 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +17 -0
- package/dist/markdownCharacterReferences.d.ts +135 -0
- package/dist/markdownCharacterReferences.d.ts.map +1 -0
- package/dist/markdownCharacterReferences.js +1127 -0
- package/dist/markdownCodeFence.d.ts +4 -0
- package/dist/markdownCodeFence.d.ts.map +1 -0
- package/dist/markdownCodeFence.js +167 -0
- package/dist/markdownExtensionNode.d.ts +88 -0
- package/dist/markdownExtensionNode.d.ts.map +1 -0
- package/dist/markdownExtensionNode.js +248 -0
- package/dist/markdownExtensions.d.ts +115 -0
- package/dist/markdownExtensions.d.ts.map +1 -0
- package/dist/markdownExtensions.js +392 -0
- package/dist/markdownHardLineBreak.d.ts +25 -0
- package/dist/markdownHardLineBreak.d.ts.map +1 -0
- package/dist/markdownHardLineBreak.js +59 -0
- package/dist/markdownHeading.d.ts +20 -0
- package/dist/markdownHeading.d.ts.map +1 -0
- package/dist/markdownHeading.js +30 -0
- package/dist/markdownLink.d.ts +5 -0
- package/dist/markdownLink.d.ts.map +1 -0
- package/dist/markdownLink.js +80 -0
- package/dist/markdownListExport.d.ts +3 -0
- package/dist/markdownListExport.d.ts.map +1 -0
- package/dist/markdownListExport.js +68 -0
- package/dist/markdownListIndentation.d.ts +8 -0
- package/dist/markdownListIndentation.d.ts.map +1 -0
- package/dist/markdownListIndentation.js +101 -0
- package/dist/markdownOrderedList.d.ts +3 -0
- package/dist/markdownOrderedList.d.ts.map +1 -0
- package/dist/markdownOrderedList.js +51 -0
- package/dist/markdownQuote.d.ts +22 -0
- package/dist/markdownQuote.d.ts.map +1 -0
- package/dist/markdownQuote.js +32 -0
- package/dist/markdownSerializers.d.ts +49 -0
- package/dist/markdownSerializers.d.ts.map +1 -0
- package/dist/markdownSerializers.js +83 -0
- package/dist/markdownSource.d.ts +71 -0
- package/dist/markdownSource.d.ts.map +1 -0
- package/dist/markdownSource.js +804 -0
- package/dist/markdownSpaceRuns.d.ts +24 -0
- package/dist/markdownSpaceRuns.d.ts.map +1 -0
- package/dist/markdownSpaceRuns.js +147 -0
- package/dist/markdownTable.d.ts +35 -0
- package/dist/markdownTable.d.ts.map +1 -0
- package/dist/markdownTable.js +227 -0
- package/dist/markdownTaskList.d.ts +48 -0
- package/dist/markdownTaskList.d.ts.map +1 -0
- package/dist/markdownTaskList.js +166 -0
- package/dist/markdownThematicBreak.d.ts +3 -0
- package/dist/markdownThematicBreak.d.ts.map +1 -0
- package/dist/markdownThematicBreak.js +55 -0
- package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts +12 -0
- package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts.map +1 -0
- package/dist/privateAttributes/layerPortalBoundary.stylex.js +16 -0
- package/dist/richtext.css +213 -0
- package/dist/textSemantics.d.ts +3 -0
- package/dist/textSemantics.d.ts.map +1 -0
- package/dist/textSemantics.js +87 -0
- package/package.json +158 -3
- package/src/RichTextEditor.doc.mjs +308 -0
- package/src/RichTextEditor.tsx +1109 -0
- package/src/RichTextEditorAutoLinkPlugin.doc.mjs +43 -0
- package/src/RichTextEditorAutoLinkPlugin.tsx +126 -0
- package/src/RichTextEditorToolbar.doc.mjs +92 -0
- package/src/RichTextEditorToolbar.tsx +871 -0
- package/src/RichTextView.doc.mjs +101 -0
- package/src/RichTextView.tsx +351 -0
- package/src/codeLanguages.ts +42 -0
- package/src/codeLineTokens.ts +241 -0
- package/src/doc-mjs.d.ts +23 -0
- package/src/editorNodes.ts +44 -0
- package/src/editorTheme.ts +550 -0
- package/src/index.ts +59 -0
- package/src/internal/CodeBackspacePlugin.tsx +96 -0
- package/src/internal/CodeBlockHeaderPlugin.tsx +282 -0
- package/src/internal/CodePastePlugin.tsx +144 -0
- package/src/internal/CodeSyntaxPlugin.tsx +493 -0
- package/src/internal/CodeTabPlugin.tsx +298 -0
- package/src/internal/DisabledLinkGuardPlugin.tsx +93 -0
- package/src/internal/ImageSourceCaretPlugin.tsx +159 -0
- package/src/internal/ListItemShortcutsPlugin.tsx +404 -0
- package/src/internal/MarkdownExtensionsPlugin.tsx +289 -0
- package/src/internal/SelectAllScopePlugin.tsx +86 -0
- package/src/internal/TableColumnFloorPlugin.tsx +99 -0
- package/src/internal/TablePastePlugin.tsx +223 -0
- package/src/internal/TableScrollRegionPlugin.tsx +107 -0
- package/src/internal/TaskCheckboxPlugin.tsx +246 -0
- package/src/internal/ToolbarLinkPopover.tsx +392 -0
- package/src/linkEditing.ts +20 -0
- package/src/linkForm.tsx +156 -0
- package/src/linkUtils.ts +106 -0
- package/src/markdown.ts +25 -0
- package/src/markdownCharacterReferences.ts +1137 -0
- package/src/markdownCodeFence.ts +216 -0
- package/src/markdownExtensionNode.ts +339 -0
- package/src/markdownExtensions.ts +496 -0
- package/src/markdownHardLineBreak.ts +73 -0
- package/src/markdownHeading.ts +33 -0
- package/src/markdownLink.ts +86 -0
- package/src/markdownListExport.ts +85 -0
- package/src/markdownListIndentation.ts +105 -0
- package/src/markdownOrderedList.ts +60 -0
- package/src/markdownQuote.ts +35 -0
- package/src/markdownSerializers.ts +108 -0
- package/src/markdownSource.ts +1045 -0
- package/src/markdownSpaceRuns.ts +164 -0
- package/src/markdownTable.ts +309 -0
- package/src/markdownTaskList.ts +222 -0
- package/src/markdownThematicBreak.ts +66 -0
- package/src/privateAttributes/layerPortalBoundary.stylex.ts +18 -0
- package/src/textSemantics.ts +104 -0
package/dist/linkForm.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file linkForm.tsx
|
|
7
|
+
* @input The address being entered, a refused-address message, and the
|
|
8
|
+
* caller's apply, remove, and cancel handlers
|
|
9
|
+
* @output Exports LinkForm, the link URL form, and REMOVE_LINK_GLYPH
|
|
10
|
+
* @position Internal to RichText. RichTextEditorToolbar's link popover
|
|
11
|
+
* renders this form, so adding and editing a link share one form.
|
|
12
|
+
*/
|
|
13
|
+
import { useRef } from 'react';
|
|
14
|
+
import { IconButton } from '@blue-patterns/core/IconButton';
|
|
15
|
+
import { TextInput } from '@blue-patterns/core/TextInput';
|
|
16
|
+
import { HStack, StackItem } from '@blue-patterns/core/Layout';
|
|
17
|
+
import { Icon } from '@blue-patterns/core/Icon';
|
|
18
|
+
import { isImeKeyEvent } from '@blue-patterns/core/utils';
|
|
19
|
+
import { useTranslator } from '@blue-patterns/core/i18n';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Remove's glyph. No shared icon name means "unlink", so it is fixed
|
|
23
|
+
* component artwork until component icon slots exist.
|
|
24
|
+
*/
|
|
25
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
26
|
+
export const REMOVE_LINK_GLYPH = /*#__PURE__*/_jsx("svg", {
|
|
27
|
+
width: "16",
|
|
28
|
+
height: "16",
|
|
29
|
+
viewBox: "0 0 24 24",
|
|
30
|
+
fill: "none",
|
|
31
|
+
"aria-hidden": "true",
|
|
32
|
+
children: /*#__PURE__*/_jsx("path", {
|
|
33
|
+
d: "M18 13l2-2a5 5 0 0 0-7-7l-2 2M6 11l-2 2a5 5 0 0 0 7 7l2-2M8 2v3M2 8h3M16 22v-3M22 16h-3",
|
|
34
|
+
stroke: "currentColor",
|
|
35
|
+
strokeWidth: "2",
|
|
36
|
+
strokeLinecap: "round",
|
|
37
|
+
strokeLinejoin: "round"
|
|
38
|
+
})
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* The link form: one URL field, then Apply, Remove (only when editing a
|
|
42
|
+
* link), and Cancel as icon buttons. Enter in the field
|
|
43
|
+
* applies, and Escape anywhere in the form cancels. A refused address shows
|
|
44
|
+
* the field's error status and keeps focus in the field.
|
|
45
|
+
*/
|
|
46
|
+
export function LinkForm({
|
|
47
|
+
url,
|
|
48
|
+
error,
|
|
49
|
+
isEditing,
|
|
50
|
+
size,
|
|
51
|
+
onUrlChange,
|
|
52
|
+
onApply,
|
|
53
|
+
onRemove,
|
|
54
|
+
onCancel
|
|
55
|
+
}) {
|
|
56
|
+
const t = useTranslator();
|
|
57
|
+
const inputRef = useRef(null);
|
|
58
|
+
const apply = () => {
|
|
59
|
+
if (!onApply()) {
|
|
60
|
+
inputRef.current?.focus();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const applyLabel = isEditing ? t('@astryx.richTextEditor.link.update') : t('@astryx.richTextEditor.link.add');
|
|
64
|
+
const removeLabel = t('@astryx.richTextEditor.link.remove');
|
|
65
|
+
const cancelLabel = t('@astryx.richTextEditor.link.cancel');
|
|
66
|
+
return /*#__PURE__*/_jsxs(HStack, {
|
|
67
|
+
gap: 1,
|
|
68
|
+
vAlign: "start",
|
|
69
|
+
onKeyDown: event => {
|
|
70
|
+
// Escape that ends an IME composition is the input method's.
|
|
71
|
+
if (event.key === 'Escape' && !isImeKeyEvent(event.nativeEvent)) {
|
|
72
|
+
// Claimed here, so a surrounding layer's Escape handling leaves it
|
|
73
|
+
// to Cancel, which also gives the editor back its selection.
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
onCancel();
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
children: [/*#__PURE__*/_jsx(StackItem, {
|
|
79
|
+
size: "fill",
|
|
80
|
+
children: /*#__PURE__*/_jsx(TextInput, {
|
|
81
|
+
ref: inputRef,
|
|
82
|
+
label: t('@astryx.richTextEditor.link.url'),
|
|
83
|
+
isLabelHidden: true,
|
|
84
|
+
value: url,
|
|
85
|
+
size: size,
|
|
86
|
+
width: "100%",
|
|
87
|
+
autoComplete: "url"
|
|
88
|
+
// A URL keyboard on touch screens and no spelling marks under an
|
|
89
|
+
// address. The field stays plain text, so a bare domain reaches
|
|
90
|
+
// the link rules (sanitizeUrl), which make it an https link.
|
|
91
|
+
,
|
|
92
|
+
inputMode: "url",
|
|
93
|
+
hasSpellCheck: false,
|
|
94
|
+
placeholder: t('@astryx.richTextEditor.link.urlPlaceholder'),
|
|
95
|
+
status: error ? {
|
|
96
|
+
type: 'error',
|
|
97
|
+
message: error
|
|
98
|
+
} : undefined,
|
|
99
|
+
statusVariant: "detached",
|
|
100
|
+
onChange: onUrlChange,
|
|
101
|
+
onEnter: apply,
|
|
102
|
+
onKeyDown: event => {
|
|
103
|
+
// Applying hands focus to the editor, which must not receive the
|
|
104
|
+
// rest of this Enter as a new line over the linked words.
|
|
105
|
+
if (event.key === 'Enter' && !isImeKeyEvent(event.nativeEvent)) {
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}), /*#__PURE__*/_jsx(IconButton, {
|
|
111
|
+
label: applyLabel,
|
|
112
|
+
icon: /*#__PURE__*/_jsx(Icon, {
|
|
113
|
+
icon: "check"
|
|
114
|
+
}),
|
|
115
|
+
variant: "ghost",
|
|
116
|
+
size: size,
|
|
117
|
+
tooltip: applyLabel,
|
|
118
|
+
onClick: apply
|
|
119
|
+
}), isEditing && /*#__PURE__*/_jsx(IconButton, {
|
|
120
|
+
label: removeLabel,
|
|
121
|
+
icon: REMOVE_LINK_GLYPH,
|
|
122
|
+
variant: "ghost",
|
|
123
|
+
size: size,
|
|
124
|
+
tooltip: removeLabel,
|
|
125
|
+
onClick: onRemove
|
|
126
|
+
}), /*#__PURE__*/_jsx(IconButton, {
|
|
127
|
+
label: cancelLabel,
|
|
128
|
+
icon: /*#__PURE__*/_jsx(Icon, {
|
|
129
|
+
icon: "close"
|
|
130
|
+
}),
|
|
131
|
+
variant: "ghost",
|
|
132
|
+
size: size,
|
|
133
|
+
tooltip: cancelLabel,
|
|
134
|
+
onClick: onCancel
|
|
135
|
+
})]
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matches a bare URL for AutoLink. Adapted from the Lexical playground.
|
|
3
|
+
* Intentionally permissive on path/query so real-world URLs linkify.
|
|
4
|
+
*/
|
|
5
|
+
export declare const URL_MATCHER: RegExp;
|
|
6
|
+
/**
|
|
7
|
+
* Matches a bare email address for AutoLink (rendered as a `mailto:` link).
|
|
8
|
+
* An unquoted local part starts only at the text start, after whitespace, or
|
|
9
|
+
* after one of `<>()[]\,;:@"`. A quoted local part ends at the next quote or
|
|
10
|
+
* line break. Each run is read once, so matching time stays linear.
|
|
11
|
+
*/
|
|
12
|
+
export declare const EMAIL_MATCHER: RegExp;
|
|
13
|
+
/**
|
|
14
|
+
* Return a safe href for `url`, or `'about:blank'` if it can't be parsed or uses
|
|
15
|
+
* a disallowed scheme. A scheme-less input like `example.com` is treated as
|
|
16
|
+
* `https://example.com`. Use this before writing any user-supplied string into
|
|
17
|
+
* a link's href.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sanitizeUrl(url: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Whether `url` is a well-formed, safe (http/https/mailto/tel) URL. Used to
|
|
22
|
+
* decide whether an entered link value is acceptable before dispatching the
|
|
23
|
+
* toggle-link command. Shares the {@link SAFE_PROTOCOLS} allowlist with
|
|
24
|
+
* {@link sanitizeUrl} so the two never disagree.
|
|
25
|
+
*/
|
|
26
|
+
export declare function validateUrl(url: string): boolean;
|
|
27
|
+
//# sourceMappingURL=linkUtils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linkUtils.d.ts","sourceRoot":"","sources":["../src/linkUtils.ts"],"names":[],"mappings":"AAoCA;;;GAGG;AACH,eAAO,MAAM,WAAW,QAC0F,CAAC;AAEnH;;;;;GAKG;AACH,eAAO,MAAM,aAAa,QACyJ,CAAC;AAgBpL;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAc/C;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAWhD"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file linkUtils.ts
|
|
5
|
+
* @input Uses only the DOM URL constructor (guarded via try/catch for SSR/edge).
|
|
6
|
+
* @output Exports `sanitizeUrl`, `validateUrl`, and the URL/email matcher regexes
|
|
7
|
+
* used by the AutoLink wiring.
|
|
8
|
+
* @position @blue-patterns/richtext helper shared by RichTextEditorToolbar (link
|
|
9
|
+
* button) and the AutoLink matchers in RichTextEditor. Kept dependency-free so
|
|
10
|
+
* both can import it without pulling in extra `@lexical/*` surface.
|
|
11
|
+
*
|
|
12
|
+
* SYNC: When modified, update:
|
|
13
|
+
* - /packages/richtext/src/RichTextEditorToolbar.tsx (link button)
|
|
14
|
+
* - /packages/richtext/src/RichTextEditor.tsx (AutoLink matchers)
|
|
15
|
+
* - /packages/richtext/src/RichTextEditor.test.tsx (tests)
|
|
16
|
+
* - /packages/richtext/src/RichTextEditor.doc.mjs (link docs)
|
|
17
|
+
* - /packages/richtext/src/index.ts (exports / barrel)
|
|
18
|
+
*
|
|
19
|
+
* NOTE: Mirrors the Lexical playground's url.ts (`sanitizeUrl` / `validateUrl`
|
|
20
|
+
* / `URL_REGEX` / `EMAIL_REGEX`). Sanitization is the security-critical part:
|
|
21
|
+
* it blocks `javascript:`, `data:`, and other non-http(s)/mailto/tel schemes
|
|
22
|
+
* from being written into a LinkNode's href, so an inserted link can never
|
|
23
|
+
* smuggle in an executable URL.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Schemes a link is allowed to carry. Anything else (notably `javascript:`,
|
|
28
|
+
* `data:`, `vbscript:`) is rejected by {@link validateUrl} and coerced to
|
|
29
|
+
* `about:blank` by {@link sanitizeUrl}, so a malicious paste can't produce a
|
|
30
|
+
* clickable code-execution link.
|
|
31
|
+
*/
|
|
32
|
+
const SAFE_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
|
|
33
|
+
|
|
34
|
+
/** Matches a scheme prefix like `https:` / `mailto:` at the start of a string. */
|
|
35
|
+
const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Matches a bare URL for AutoLink. Adapted from the Lexical playground.
|
|
39
|
+
* Intentionally permissive on path/query so real-world URLs linkify.
|
|
40
|
+
*/
|
|
41
|
+
export const URL_MATCHER = /((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)/;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Matches a bare email address for AutoLink (rendered as a `mailto:` link).
|
|
45
|
+
* An unquoted local part starts only at the text start, after whitespace, or
|
|
46
|
+
* after one of `<>()[]\,;:@"`. A quoted local part ends at the next quote or
|
|
47
|
+
* line break. Each run is read once, so matching time stays linear.
|
|
48
|
+
*/
|
|
49
|
+
export const EMAIL_MATCHER = /((?<![^<>()[\]\\,;:\s@"])[^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*|("[^"\r\n]+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A host with a port also looks like a scheme (`example.com:`).
|
|
53
|
+
* The host needs a dot or is `localhost`, so `tel:911` keeps its scheme.
|
|
54
|
+
*/
|
|
55
|
+
const HOST_WITH_PORT = /^(?:localhost|[a-z0-9-]+(?:\.[a-z0-9-]+)+):\d{1,5}(?:[/?#]|$)/i;
|
|
56
|
+
|
|
57
|
+
/** Prepend a default `https://` scheme to a bare host so it parses as absolute. */
|
|
58
|
+
function withDefaultScheme(url) {
|
|
59
|
+
return HAS_SCHEME.test(url) && !HOST_WITH_PORT.test(url) ? url : `https://${url}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Return a safe href for `url`, or `'about:blank'` if it can't be parsed or uses
|
|
64
|
+
* a disallowed scheme. A scheme-less input like `example.com` is treated as
|
|
65
|
+
* `https://example.com`. Use this before writing any user-supplied string into
|
|
66
|
+
* a link's href.
|
|
67
|
+
*/
|
|
68
|
+
export function sanitizeUrl(url) {
|
|
69
|
+
const trimmed = url.trim();
|
|
70
|
+
if (trimmed === '') {
|
|
71
|
+
return 'about:blank';
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const parsed = new URL(withDefaultScheme(trimmed));
|
|
75
|
+
if (!SAFE_PROTOCOLS.has(parsed.protocol.toLowerCase())) {
|
|
76
|
+
return 'about:blank';
|
|
77
|
+
}
|
|
78
|
+
return parsed.href;
|
|
79
|
+
} catch {
|
|
80
|
+
return 'about:blank';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Whether `url` is a well-formed, safe (http/https/mailto/tel) URL. Used to
|
|
86
|
+
* decide whether an entered link value is acceptable before dispatching the
|
|
87
|
+
* toggle-link command. Shares the {@link SAFE_PROTOCOLS} allowlist with
|
|
88
|
+
* {@link sanitizeUrl} so the two never disagree.
|
|
89
|
+
*/
|
|
90
|
+
export function validateUrl(url) {
|
|
91
|
+
const trimmed = url.trim();
|
|
92
|
+
if (trimmed === '') {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const parsed = new URL(withDefaultScheme(trimmed));
|
|
97
|
+
return SAFE_PROTOCOLS.has(parsed.protocol.toLowerCase());
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file markdown.ts
|
|
3
|
+
* @output Server-safe entry `@blue-patterns/richtext/markdown`: the Markdown
|
|
4
|
+
* serializers, createRichTextExtension, RichTextExtensionError, and their
|
|
5
|
+
* types.
|
|
6
|
+
* @position Imports no React, DOM, or client-only module, so server and Node
|
|
7
|
+
* code converts Markdown and adopts plugins here (spec:AST-064 FR3, FR12).
|
|
8
|
+
* The package's main entry is a client module and re-exports all of this
|
|
9
|
+
* for client code. An extension holds its plugin's functions, so it is not
|
|
10
|
+
* serializable: create the extensions a page passes to the surfaces in a
|
|
11
|
+
* client module, and create your own for headless use.
|
|
12
|
+
*/
|
|
13
|
+
export { markdownToEditorStateJSON, editorStateJSONToMarkdown, } from './markdownSerializers.js';
|
|
14
|
+
export type { MarkdownSerializerOptions } from './markdownSerializers.js';
|
|
15
|
+
export { createRichTextExtension, RichTextExtensionError, } from './markdownExtensions.js';
|
|
16
|
+
export type { RichTextMarkdownExtension } from './markdownExtensions.js';
|
|
17
|
+
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH,OAAO,EACL,yBAAyB,EACzB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAC,yBAAyB,EAAC,MAAM,uBAAuB,CAAC;AACrE,OAAO,EACL,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAC,yBAAyB,EAAC,MAAM,sBAAsB,CAAC"}
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file markdown.ts
|
|
5
|
+
* @output Server-safe entry `@blue-patterns/richtext/markdown`: the Markdown
|
|
6
|
+
* serializers, createRichTextExtension, RichTextExtensionError, and their
|
|
7
|
+
* types.
|
|
8
|
+
* @position Imports no React, DOM, or client-only module, so server and Node
|
|
9
|
+
* code converts Markdown and adopts plugins here (spec:AST-064 FR3, FR12).
|
|
10
|
+
* The package's main entry is a client module and re-exports all of this
|
|
11
|
+
* for client code. An extension holds its plugin's functions, so it is not
|
|
12
|
+
* serializable: create the extensions a page passes to the surfaces in a
|
|
13
|
+
* client module, and create your own for headless use.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export { markdownToEditorStateJSON, editorStateJSONToMarkdown } from "./markdownSerializers.js";
|
|
17
|
+
export { createRichTextExtension, RichTextExtensionError } from "./markdownExtensions.js";
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { type ElementNode, type LexicalNode } from 'lexical';
|
|
2
|
+
/**
|
|
3
|
+
* What a stand-in replaced: a reference as written, an escaped `&`, or a
|
|
4
|
+
* backslash-escaped character.
|
|
5
|
+
*/
|
|
6
|
+
type StandIn = {
|
|
7
|
+
readonly kind: 'reference';
|
|
8
|
+
readonly source: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'ampersand';
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: 'escape';
|
|
13
|
+
readonly character: string;
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: 'backslash';
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: 'literal';
|
|
18
|
+
readonly text: string;
|
|
19
|
+
};
|
|
20
|
+
export interface ProtectedMarkdown {
|
|
21
|
+
/** The source with stand-ins in place of references. */
|
|
22
|
+
readonly markdown: string;
|
|
23
|
+
/** What each stand-in replaced. */
|
|
24
|
+
readonly standIns: ReadonlyMap<string, StandIn>;
|
|
25
|
+
}
|
|
26
|
+
/** Private-use characters that do not occur in `text`, in order. */
|
|
27
|
+
export declare function absentCharacters(text: string): Generator<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Returns `markdown` with every backslash escape outside code — a backslash
|
|
30
|
+
* before ASCII punctuation — replaced by a private-use stand-in for the
|
|
31
|
+
* escaped character, and every backslash in code by a stand-in for itself,
|
|
32
|
+
* which Lexical would otherwise drop from a code span — except before `|`,
|
|
33
|
+
* which a table reads before its code. A backslash before anything else
|
|
34
|
+
* outside code stays as written. One pass over the source.
|
|
35
|
+
*/
|
|
36
|
+
export declare function protectBackslashEscapes(markdown: string): ProtectedMarkdown;
|
|
37
|
+
/**
|
|
38
|
+
* Whether core Markdown refuses a link to `url`, a destination already
|
|
39
|
+
* decoded: the probe writes it back with every special character escaped.
|
|
40
|
+
* A URL that cannot be written back is refused.
|
|
41
|
+
*/
|
|
42
|
+
export declare function coreRefusesUrl(url: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Returns `markdown` with each inline link whose destination core Markdown
|
|
45
|
+
* refuses — `javascript:` and the like — kept from becoming a link
|
|
46
|
+
* (spec:AST-061 FR7). Where core shows the link as its source text, exactly
|
|
47
|
+
* as written, the whole source becomes one stand-in that comes back as that
|
|
48
|
+
* text. Where core reads no link there at all (its link text holds brackets
|
|
49
|
+
* of its own), only the `(` that opens the destination becomes a stand-in,
|
|
50
|
+
* so the text around it reads as core reads it, marks included. Brackets and
|
|
51
|
+
* parentheses pair by nesting, so link text holding brackets is found whole.
|
|
52
|
+
* Linear in the source: pairs come from one pass, and a destination inside a
|
|
53
|
+
* found link is not probed again. Run first, on the source as written;
|
|
54
|
+
* `$unwrapRefusedLinks` catches any link the editor still makes.
|
|
55
|
+
*/
|
|
56
|
+
export declare function protectRefusedLinks(markdown: string): ProtectedMarkdown;
|
|
57
|
+
/** Whether `node` is text that holds an image's Markdown source. */
|
|
58
|
+
export declare function $isImageSource(node: LexicalNode): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Returns `markdown` with every image outside code replaced by a private-use
|
|
61
|
+
* stand-in, and the source each stand-in replaced. An image starts with a `!`
|
|
62
|
+
* that is not escaped and the paired brackets of its text. An inline image
|
|
63
|
+
* (``) ends with the destination and title that close it, read whole
|
|
64
|
+
* by core's inline parser. A reference image (`![a][b]`, `![a][]`, `![a]`)
|
|
65
|
+
* ends with its label; its definition can sit in another block, so it is kept
|
|
66
|
+
* whether or not one exists. A link around an image keeps its own brackets,
|
|
67
|
+
* so the image stays inside the link's text.
|
|
68
|
+
*/
|
|
69
|
+
export declare function protectImages(markdown: string): {
|
|
70
|
+
readonly markdown: string;
|
|
71
|
+
readonly images: ReadonlyMap<string, string>;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Puts each image's source back where its stand-in sits under `node`, as
|
|
75
|
+
* text marked as an image's source. The text is a token: a person deletes it
|
|
76
|
+
* whole, and autolinking leaves its URL alone. ImageSourceCaretPlugin keeps
|
|
77
|
+
* the caret out of it, so typing never replaces it.
|
|
78
|
+
*/
|
|
79
|
+
export declare function $restoreImages(node: ElementNode, images: ReadonlyMap<string, string>): void;
|
|
80
|
+
/**
|
|
81
|
+
* Takes every link under `node` whose URL core Markdown refuses out of its
|
|
82
|
+
* link, leaving its text in place, so no form of link text or destination
|
|
83
|
+
* the source pass did not find can store or draw an unsafe URL.
|
|
84
|
+
*/
|
|
85
|
+
export declare function $unwrapRefusedLinks(node: ElementNode): void;
|
|
86
|
+
/**
|
|
87
|
+
* Returns `markdown` with every parenthesis inside an inline link
|
|
88
|
+
* destination — between the destination's own parentheses, balanced, as core
|
|
89
|
+
* Markdown and CommonMark read them — replaced by a private-use stand-in for
|
|
90
|
+
* itself, outside code. Run after protectBackslashEscapes, so an escaped
|
|
91
|
+
* parenthesis is already a stand-in and never counts toward the balance.
|
|
92
|
+
*/
|
|
93
|
+
export declare function protectLinkDestinationParentheses(markdown: string): ProtectedMarkdown;
|
|
94
|
+
/** The code spans a stand-in pass replaced: each stand-in's code. */
|
|
95
|
+
export interface ProtectedCodeSpans {
|
|
96
|
+
readonly markdown: string;
|
|
97
|
+
readonly spans: ReadonlyMap<string, string>;
|
|
98
|
+
/** Stand-ins for backticks in link destinations and titles. */
|
|
99
|
+
readonly standIns: ReadonlyMap<string, StandIn>;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Returns `markdown` with each code span outside fenced code replaced by a
|
|
103
|
+
* private-use stand-in, and the code each stands for, as core Markdown reads
|
|
104
|
+
* it. Code spans bind tighter than emphasis and links (CommonMark 0.31
|
|
105
|
+
* §6.1), but Lexical's import applies a code span before an earlier mark on
|
|
106
|
+
* the same line, which breaks every mark around code that follows another
|
|
107
|
+
* (`~~a~~ ~~`c`~~`); with the code out of the way, the marks pair first and
|
|
108
|
+
* the code comes back inside them. Backticks in a link's destination or
|
|
109
|
+
* title are the link's, never a code span: they become stand-ins too, put
|
|
110
|
+
* back into the link's address and title after import, so Lexical cannot
|
|
111
|
+
* pair them into code either. In a table row, a span holding an unescaped
|
|
112
|
+
* `|` is left alone, so the row still splits its cells there, as GFM does.
|
|
113
|
+
*/
|
|
114
|
+
export declare function protectCodeSpans(markdown: string): ProtectedCodeSpans;
|
|
115
|
+
/**
|
|
116
|
+
* Puts each code span back where its stand-in sits under `node`: the code as
|
|
117
|
+
* text with the code format, inside whatever marks the stand-in took. In a
|
|
118
|
+
* table cell an escaped `\|` in code is a `|`, as GFM reads cells.
|
|
119
|
+
*/
|
|
120
|
+
export declare function $restoreCodeSpans(node: ElementNode, spans: ReadonlyMap<string, string>): void;
|
|
121
|
+
/**
|
|
122
|
+
* Returns `markdown` with every character reference outside code replaced by
|
|
123
|
+
* a private-use stand-in that occurs nowhere else in it, and the escaped `&`
|
|
124
|
+
* of an escaped reference replaced by one too. One pass over the source.
|
|
125
|
+
*/
|
|
126
|
+
export declare function protectCharacterReferences(markdown: string): ProtectedMarkdown;
|
|
127
|
+
/**
|
|
128
|
+
* Replaces the stand-ins under `node` with the characters their references
|
|
129
|
+
* name or their escapes stand for — or, in code Lexical read as code, with
|
|
130
|
+
* the references and escapes as written — in text, link destinations, and
|
|
131
|
+
* link titles. One pass over the text.
|
|
132
|
+
*/
|
|
133
|
+
export declare function $restoreCharacterReferences(node: ElementNode, standIns: ReadonlyMap<string, StandIn>): void;
|
|
134
|
+
export {};
|
|
135
|
+
//# sourceMappingURL=markdownCharacterReferences.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdownCharacterReferences.d.ts","sourceRoot":"","sources":["../src/markdownCharacterReferences.ts"],"names":[],"mappings":"AAiCA,OAAO,EAML,KAAK,WAAW,EAChB,KAAK,WAAW,EACjB,MAAM,SAAS,CAAC;AAMjB;;;GAGG;AACH,KAAK,OAAO,GACR;IAAC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GACrD;IAAC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAC,GAC5B;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAC,GACrD;IAAC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAC,GAC5B;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,mCAAmC;IACnC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjD;AAYD,oEAAoE;AACpE,wBAAiB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,CAUjE;AAkID;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CA2D3E;AAkHD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAUnD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CA8DvE;AAUD,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAEzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9C,CA+DA;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,IAAI,CA8BN;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAS3D;AAED;;;;;;GAMG;AACH,wBAAgB,iCAAiC,CAC/C,QAAQ,EAAE,MAAM,GACf,iBAAiB,CA4EnB;AAED,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjD;AA2FD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,CAwErE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,IAAI,CAgCN;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,MAAM,GACf,iBAAiB,CA+EnB;AAmDD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,IAAI,CAsCN"}
|