testomatio-editor-blocks 0.4.53 → 0.4.55
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/package/editor/blocks/markdown.js +97 -36
- package/package/editor/blocks/step.js +1 -1
- package/package/editor/blocks/stepField.d.ts +2 -1
- package/package/editor/blocks/stepField.js +16 -8
- package/package/editor/blocks/useAutoResize.d.ts +3 -4
- package/package/editor/blocks/useAutoResize.js +57 -45
- package/package/editor/customMarkdownConverter.js +66 -26
- package/package.json +1 -1
- package/src/App.tsx +15 -3
- package/src/editor/blocks/markdown.ts +110 -40
- package/src/editor/blocks/step.tsx +1 -0
- package/src/editor/blocks/stepField.tsx +18 -8
- package/src/editor/blocks/useAutoResize.ts +60 -50
- package/src/editor/customMarkdownConverter.ts +80 -27
- package/src/editor/markdownToBlocks.test.ts +39 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+?)(?:\s+=\d+x(?:\d+|\*))?\)/g;
|
|
2
2
|
const MARKDOWN_ESCAPE_REGEX = /([*_\\])/g;
|
|
3
|
-
const INLINE_SEGMENT_REGEX = /(\*\*\*[^*]+\*\*\*|___[^_]+___|\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|<u>[^<]+<\/u>)/;
|
|
4
3
|
export function escapeHtml(text) {
|
|
5
4
|
return text
|
|
6
5
|
.replace(/&/g, "&")
|
|
@@ -12,46 +11,108 @@ export function escapeHtml(text) {
|
|
|
12
11
|
function restoreEscapes(text) {
|
|
13
12
|
return text.replace(/\uE000/g, "\\");
|
|
14
13
|
}
|
|
14
|
+
function findItalicClose(text, start, marker) {
|
|
15
|
+
let j = start;
|
|
16
|
+
while (j < text.length) {
|
|
17
|
+
const ch = text[j];
|
|
18
|
+
if (ch === "\uE000") {
|
|
19
|
+
j += 2;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if ((ch === "*" || ch === "_") && text[j + 1] === ch) {
|
|
23
|
+
const close = text.indexOf(ch + ch, j + 2);
|
|
24
|
+
if (close === -1) {
|
|
25
|
+
return -1;
|
|
26
|
+
}
|
|
27
|
+
j = close + 2;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (ch === marker) {
|
|
31
|
+
return j;
|
|
32
|
+
}
|
|
33
|
+
j += 1;
|
|
34
|
+
}
|
|
35
|
+
return -1;
|
|
36
|
+
}
|
|
37
|
+
function parseInlineSegments(normalized, outer) {
|
|
38
|
+
const result = [];
|
|
39
|
+
let buffer = "";
|
|
40
|
+
const pushPlain = () => {
|
|
41
|
+
if (!buffer)
|
|
42
|
+
return;
|
|
43
|
+
result.push({ text: restoreEscapes(buffer), styles: { ...outer } });
|
|
44
|
+
buffer = "";
|
|
45
|
+
};
|
|
46
|
+
const wrap = (inner, add) => {
|
|
47
|
+
pushPlain();
|
|
48
|
+
result.push(...parseInlineSegments(inner, { ...outer, ...add }));
|
|
49
|
+
};
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < normalized.length) {
|
|
52
|
+
if (normalized.startsWith("***", i)) {
|
|
53
|
+
const end = normalized.indexOf("***", i + 3);
|
|
54
|
+
if (end !== -1) {
|
|
55
|
+
wrap(normalized.slice(i + 3, end), { bold: true, italic: true });
|
|
56
|
+
i = end + 3;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (normalized.startsWith("___", i)) {
|
|
61
|
+
const end = normalized.indexOf("___", i + 3);
|
|
62
|
+
if (end !== -1) {
|
|
63
|
+
wrap(normalized.slice(i + 3, end), { bold: true, italic: true });
|
|
64
|
+
i = end + 3;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (normalized.startsWith("**", i)) {
|
|
69
|
+
const end = normalized.indexOf("**", i + 2);
|
|
70
|
+
if (end !== -1) {
|
|
71
|
+
wrap(normalized.slice(i + 2, end), { bold: true });
|
|
72
|
+
i = end + 2;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (normalized.startsWith("__", i)) {
|
|
77
|
+
const end = normalized.indexOf("__", i + 2);
|
|
78
|
+
if (end !== -1) {
|
|
79
|
+
wrap(normalized.slice(i + 2, end), { bold: true });
|
|
80
|
+
i = end + 2;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (normalized.startsWith("<u>", i)) {
|
|
85
|
+
const end = normalized.indexOf("</u>", i + 3);
|
|
86
|
+
if (end !== -1) {
|
|
87
|
+
wrap(normalized.slice(i + 3, end), { underline: true });
|
|
88
|
+
i = end + 4;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (normalized[i] === "*" || normalized[i] === "_") {
|
|
93
|
+
const marker = normalized[i];
|
|
94
|
+
const end = findItalicClose(normalized, i + 1, marker);
|
|
95
|
+
if (end !== -1) {
|
|
96
|
+
wrap(normalized.slice(i + 1, end), { italic: true });
|
|
97
|
+
i = end + 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
buffer += normalized[i];
|
|
102
|
+
i += 1;
|
|
103
|
+
}
|
|
104
|
+
pushPlain();
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
15
107
|
function parseInlineMarkdown(text) {
|
|
16
108
|
if (!text) {
|
|
17
109
|
return [];
|
|
18
110
|
}
|
|
19
111
|
const normalized = text.replace(/\\([*_`~])/g, "\uE000$1");
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const content = segment.slice(3, -3);
|
|
25
|
-
return {
|
|
26
|
-
text: restoreEscapes(content),
|
|
27
|
-
styles: { bold: true, italic: true, underline: false },
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
if (/^\*\*(.+)\*\*$/.test(segment) || /^__(.+)__$/.test(segment)) {
|
|
31
|
-
const content = segment.slice(2, -2);
|
|
32
|
-
return {
|
|
33
|
-
text: restoreEscapes(content),
|
|
34
|
-
styles: { ...baseStyles, bold: true },
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
if (/^\*(.+)\*$/.test(segment) || /^_(.+)_$/.test(segment)) {
|
|
38
|
-
const content = segment.slice(1, -1);
|
|
39
|
-
return {
|
|
40
|
-
text: restoreEscapes(content),
|
|
41
|
-
styles: { ...baseStyles, italic: true },
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
if (/^<u>(.+)<\/u>$/.test(segment)) {
|
|
45
|
-
const content = segment.slice(3, -4);
|
|
46
|
-
return {
|
|
47
|
-
text: restoreEscapes(content),
|
|
48
|
-
styles: { ...baseStyles, underline: true },
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
text: restoreEscapes(segment),
|
|
53
|
-
styles: { ...baseStyles },
|
|
54
|
-
};
|
|
112
|
+
return parseInlineSegments(normalized, {
|
|
113
|
+
bold: false,
|
|
114
|
+
italic: false,
|
|
115
|
+
underline: false,
|
|
55
116
|
});
|
|
56
117
|
}
|
|
57
118
|
function inlineToHtml(inline) {
|
|
@@ -404,7 +404,7 @@ export const stepBlock = createReactBlockSpec({
|
|
|
404
404
|
if (!effectiveVertical) {
|
|
405
405
|
return (_jsx(StepHorizontalView, { ref: containerRef, blockId: block.id, stepNumber: stepNumber, stepValue: combinedStepValue, expectedResult: expectedResult, onStepChange: handleCombinedStepChange, onExpectedChange: handleExpectedChange, onInsertNextStep: handleInsertNextStep, onFieldFocus: handleFieldFocus, viewToggle: viewToggleButton }));
|
|
406
406
|
}
|
|
407
|
-
return (_jsxs("div", { className: "bn-teststep", "data-block-id": block.id, ref: containerRef, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsxs("div", { className: "bn-teststep__header", children: [_jsx("span", { className: "bn-teststep__title", children: "Step" }), viewToggleButton] }), _jsx(StepField, { label: "Step", showLabel: false, value: stepTitle, placeholder: STEP_TITLE_PLACEHOLDER, onChange: handleStepTitleChange, autoFocus: stepTitle.length === 0, multiline: true, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: handleFieldFocus, enableImageUpload: false, showFormattingButtons: true, onImageFile: async (file) => {
|
|
407
|
+
return (_jsxs("div", { className: "bn-teststep", "data-block-id": block.id, ref: containerRef, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsxs("div", { className: "bn-teststep__header", children: [_jsx("span", { className: "bn-teststep__title", children: "Step" }), viewToggleButton] }), _jsx(StepField, { label: "Step", showLabel: false, value: stepTitle, placeholder: STEP_TITLE_PLACEHOLDER, onChange: handleStepTitleChange, autoFocus: stepTitle.length === 0, multiline: true, disableNewlines: true, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: handleFieldFocus, enableImageUpload: false, showFormattingButtons: true, onImageFile: async (file) => {
|
|
408
408
|
if (!uploadImage) {
|
|
409
409
|
return;
|
|
410
410
|
}
|
|
@@ -16,6 +16,7 @@ type StepFieldProps = {
|
|
|
16
16
|
autoFocus?: boolean;
|
|
17
17
|
focusSignal?: number;
|
|
18
18
|
multiline?: boolean;
|
|
19
|
+
disableNewlines?: boolean;
|
|
19
20
|
enableAutocomplete?: boolean;
|
|
20
21
|
fieldName?: string;
|
|
21
22
|
suggestionFilter?: (suggestion: Suggestion) => boolean;
|
|
@@ -53,5 +54,5 @@ export declare function applyInlineExclusion(formatting: FormattingMeta[], links
|
|
|
53
54
|
links: LinkMeta[];
|
|
54
55
|
};
|
|
55
56
|
export declare function buildFullMarkdown(plainText: string, links: LinkMeta[], formatting: FormattingMeta[]): string;
|
|
56
|
-
export declare function StepField({ label, showLabel, labelToggle, labelAction, placeholder, value, onChange, autoFocus, focusSignal, multiline, enableAutocomplete, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly, showSuggestionsOnFocus, enableImageUpload, onImageFile, rightAction, showFormattingButtons, showImageButton, onFieldFocus, }: StepFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
57
|
+
export declare function StepField({ label, showLabel, labelToggle, labelAction, placeholder, value, onChange, autoFocus, focusSignal, multiline, disableNewlines, enableAutocomplete, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly, showSuggestionsOnFocus, enableImageUpload, onImageFile, rightAction, showFormattingButtons, showImageButton, onFieldFocus, }: StepFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
57
58
|
export {};
|
|
@@ -532,7 +532,7 @@ function markdownToPlainText(markdown) {
|
|
|
532
532
|
return markdown.replace(/!\[[^\]]*]\([^)]+\)/g, "").replace(/\[[^\]]*]\([^)]+\)/g, "").replace(/[*_`~]/g, "").replace(/\s+/g, " ").trim();
|
|
533
533
|
}
|
|
534
534
|
}
|
|
535
|
-
export function StepField({ label, showLabel = true, labelToggle, labelAction, placeholder, value, onChange, autoFocus, focusSignal, multiline = false, enableAutocomplete = false, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly = false, showSuggestionsOnFocus = false, enableImageUpload = false, onImageFile, rightAction, showFormattingButtons = false, showImageButton = false, onFieldFocus, }) {
|
|
535
|
+
export function StepField({ label, showLabel = true, labelToggle, labelAction, placeholder, value, onChange, autoFocus, focusSignal, multiline = false, disableNewlines = false, enableAutocomplete = false, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly = false, showSuggestionsOnFocus = false, enableImageUpload = false, onImageFile, rightAction, showFormattingButtons = false, showImageButton = false, onFieldFocus, }) {
|
|
536
536
|
var _a, _b;
|
|
537
537
|
const stepSuggestions = useStepAutocomplete();
|
|
538
538
|
const suggestions = suggestionsOverride !== null && suggestionsOverride !== void 0 ? suggestionsOverride : stepSuggestions;
|
|
@@ -753,6 +753,15 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
753
753
|
}
|
|
754
754
|
textareaNode.focus();
|
|
755
755
|
}, [focusSignal, textareaNode]);
|
|
756
|
+
useAutoResize({
|
|
757
|
+
textarea: textareaNode,
|
|
758
|
+
enabled: multiline,
|
|
759
|
+
onResize: useCallback(() => {
|
|
760
|
+
var _a;
|
|
761
|
+
const instance = editorInstanceRef.current;
|
|
762
|
+
(_a = instance === null || instance === void 0 ? void 0 : instance._updateAutoHeight) === null || _a === void 0 ? void 0 : _a.call(instance);
|
|
763
|
+
}, []),
|
|
764
|
+
});
|
|
756
765
|
useEffect(() => {
|
|
757
766
|
var _a;
|
|
758
767
|
const instance = editorInstanceRef.current;
|
|
@@ -814,12 +823,6 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
814
823
|
}
|
|
815
824
|
textareaNode.readOnly = readOnly;
|
|
816
825
|
}, [readOnly, textareaNode]);
|
|
817
|
-
useAutoResize({
|
|
818
|
-
textarea: textareaNode,
|
|
819
|
-
multiline,
|
|
820
|
-
minRows: 3,
|
|
821
|
-
maxRows: 16,
|
|
822
|
-
});
|
|
823
826
|
useEffect(() => {
|
|
824
827
|
if (!textareaNode) {
|
|
825
828
|
return;
|
|
@@ -1387,6 +1390,11 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1387
1390
|
setShowAllSuggestions(true);
|
|
1388
1391
|
return;
|
|
1389
1392
|
}
|
|
1393
|
+
if (disableNewlines && (event.key === "Enter" || event.code === "Enter")) {
|
|
1394
|
+
event.preventDefault();
|
|
1395
|
+
event.stopImmediatePropagation();
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1390
1398
|
if (event.key === "Tab") {
|
|
1391
1399
|
const moved = focusAdjacentField(event.shiftKey ? -1 : 1);
|
|
1392
1400
|
if (moved) {
|
|
@@ -1395,7 +1403,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1395
1403
|
}
|
|
1396
1404
|
}
|
|
1397
1405
|
};
|
|
1398
|
-
}, [activeSuggestionIndex, applySuggestion, enableAutocomplete, filteredSuggestions, focusAdjacentField, handleToolbarAction, readOnly, shouldShowAutocomplete]);
|
|
1406
|
+
}, [activeSuggestionIndex, applySuggestion, disableNewlines, enableAutocomplete, filteredSuggestions, focusAdjacentField, handleToolbarAction, readOnly, shouldShowAutocomplete]);
|
|
1399
1407
|
useEffect(() => {
|
|
1400
1408
|
if (!textareaNode) {
|
|
1401
1409
|
return;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
type Options = {
|
|
2
2
|
textarea: HTMLTextAreaElement | null;
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
maxRows?: number;
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
onResize: () => void;
|
|
6
5
|
};
|
|
7
|
-
export declare function useAutoResize({ textarea,
|
|
6
|
+
export declare function useAutoResize({ textarea, enabled, onResize }: Options): () => void;
|
|
8
7
|
export {};
|
|
@@ -1,65 +1,77 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
2
|
-
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
// Shared across all hook instances: one fonts.ready promise per document,
|
|
3
|
+
// fanned out to every registered callback so N fields cost 1 .then().
|
|
4
|
+
const fontsReadyCallbacks = new Set();
|
|
5
|
+
let fontsReadyAttached = false;
|
|
6
|
+
function registerFontsReady(cb) {
|
|
7
|
+
var _a;
|
|
8
|
+
if (typeof document === "undefined" || !((_a = document.fonts) === null || _a === void 0 ? void 0 : _a.ready)) {
|
|
9
|
+
return () => { };
|
|
10
|
+
}
|
|
11
|
+
fontsReadyCallbacks.add(cb);
|
|
12
|
+
if (!fontsReadyAttached) {
|
|
13
|
+
fontsReadyAttached = true;
|
|
14
|
+
document.fonts.ready
|
|
15
|
+
.then(() => {
|
|
16
|
+
for (const fn of fontsReadyCallbacks)
|
|
17
|
+
fn();
|
|
18
|
+
})
|
|
19
|
+
.catch(() => { });
|
|
20
|
+
}
|
|
21
|
+
return () => {
|
|
22
|
+
fontsReadyCallbacks.delete(cb);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function useAutoResize({ textarea, enabled = true, onResize }) {
|
|
3
26
|
const frameRef = useRef(0);
|
|
27
|
+
const onResizeRef = useRef(onResize);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
onResizeRef.current = onResize;
|
|
30
|
+
}, [onResize]);
|
|
4
31
|
useEffect(() => {
|
|
5
|
-
|
|
6
|
-
if (!textarea || !multiline) {
|
|
32
|
+
if (!textarea || !enabled)
|
|
7
33
|
return;
|
|
8
|
-
}
|
|
9
|
-
const resize = () => {
|
|
10
|
-
textarea.style.height = "auto";
|
|
11
|
-
const lineHeight = parseFloat(getComputedStyle(textarea).lineHeight || "16");
|
|
12
|
-
const minHeight = lineHeight * minRows;
|
|
13
|
-
const maxHeight = lineHeight * maxRows;
|
|
14
|
-
const clampedHeight = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);
|
|
15
|
-
textarea.style.height = `${clampedHeight}px`;
|
|
16
|
-
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? "auto" : "hidden";
|
|
17
|
-
};
|
|
18
|
-
const mutationObserver = new MutationObserver(resize);
|
|
19
|
-
mutationObserver.observe(textarea, { childList: true, characterData: true, subtree: true });
|
|
20
|
-
const resizeObserver = new ResizeObserver(resize);
|
|
21
|
-
resizeObserver.observe(textarea);
|
|
22
|
-
const handleInput = () => {
|
|
23
|
-
var _a;
|
|
24
|
-
cancelAnimationFrame((_a = frameRef.current) !== null && _a !== void 0 ? _a : 0);
|
|
25
|
-
frameRef.current = requestAnimationFrame(resize);
|
|
26
|
-
};
|
|
27
|
-
textarea.addEventListener("input", handleInput);
|
|
28
34
|
let cancelled = false;
|
|
29
|
-
|
|
35
|
+
// All callers go through this coalescing scheduler: repeated pings in a
|
|
36
|
+
// single frame collapse to one reflow.
|
|
37
|
+
const schedule = () => {
|
|
38
|
+
if (cancelled)
|
|
39
|
+
return;
|
|
40
|
+
if (frameRef.current)
|
|
41
|
+
return;
|
|
30
42
|
frameRef.current = requestAnimationFrame(() => {
|
|
43
|
+
frameRef.current = 0;
|
|
31
44
|
if (!cancelled)
|
|
32
|
-
|
|
45
|
+
onResizeRef.current();
|
|
33
46
|
});
|
|
34
|
-
}
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
//
|
|
47
|
+
};
|
|
48
|
+
// Initial pass after layout.
|
|
49
|
+
schedule();
|
|
50
|
+
// One-shot: re-run once the textarea actually enters the layout tree.
|
|
51
|
+
// This is the piece that fixes the drag-drop remount and
|
|
52
|
+
// snippet-insert-while-hidden cases OverType itself cannot recover from.
|
|
38
53
|
const intersectionObserver = new IntersectionObserver((entries) => {
|
|
39
54
|
for (const entry of entries) {
|
|
40
|
-
if (entry.isIntersecting
|
|
41
|
-
|
|
55
|
+
if (entry.isIntersecting) {
|
|
56
|
+
schedule();
|
|
42
57
|
intersectionObserver.disconnect();
|
|
43
58
|
break;
|
|
44
59
|
}
|
|
45
60
|
}
|
|
46
61
|
});
|
|
47
62
|
intersectionObserver.observe(textarea);
|
|
48
|
-
|
|
49
|
-
document.fonts.ready.then(() => {
|
|
50
|
-
if (!cancelled)
|
|
51
|
-
resize();
|
|
52
|
-
}).catch(() => { });
|
|
53
|
-
}
|
|
63
|
+
const unregisterFonts = registerFontsReady(schedule);
|
|
54
64
|
return () => {
|
|
55
|
-
var _a;
|
|
56
65
|
cancelled = true;
|
|
57
|
-
mutationObserver.disconnect();
|
|
58
|
-
resizeObserver.disconnect();
|
|
59
66
|
intersectionObserver.disconnect();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
unregisterFonts();
|
|
68
|
+
if (frameRef.current) {
|
|
69
|
+
cancelAnimationFrame(frameRef.current);
|
|
70
|
+
frameRef.current = 0;
|
|
71
|
+
}
|
|
63
72
|
};
|
|
64
|
-
}, [textarea,
|
|
73
|
+
}, [textarea, enabled]);
|
|
74
|
+
return useCallback(() => {
|
|
75
|
+
onResizeRef.current();
|
|
76
|
+
}, []);
|
|
65
77
|
}
|
|
@@ -521,28 +521,80 @@ export function blocksToMarkdown(blocks) {
|
|
|
521
521
|
return cleaned;
|
|
522
522
|
}
|
|
523
523
|
function parseInlineMarkdown(text) {
|
|
524
|
-
|
|
524
|
+
return parseInlineSegments(stripHtmlWrappers(text), {});
|
|
525
|
+
}
|
|
526
|
+
function findItalicClose(text, start, marker) {
|
|
527
|
+
let j = start;
|
|
528
|
+
while (j < text.length) {
|
|
529
|
+
const ch = text[j];
|
|
530
|
+
if (ch === "\\") {
|
|
531
|
+
j += 2;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if ((ch === "*" || ch === "_") && text[j + 1] === ch) {
|
|
535
|
+
const close = text.indexOf(ch + ch, j + 2);
|
|
536
|
+
if (close === -1) {
|
|
537
|
+
return -1;
|
|
538
|
+
}
|
|
539
|
+
j = close + 2;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (ch === marker) {
|
|
543
|
+
return j;
|
|
544
|
+
}
|
|
545
|
+
j += 1;
|
|
546
|
+
}
|
|
547
|
+
return -1;
|
|
548
|
+
}
|
|
549
|
+
function parseInlineSegments(cleaned, outerStyles) {
|
|
525
550
|
const result = [];
|
|
526
551
|
let buffer = "";
|
|
527
552
|
const pushPlain = () => {
|
|
528
553
|
if (buffer.length === 0) {
|
|
529
554
|
return;
|
|
530
555
|
}
|
|
531
|
-
result.push({
|
|
556
|
+
result.push({
|
|
557
|
+
type: "text",
|
|
558
|
+
text: unescapeMarkdown(buffer),
|
|
559
|
+
styles: { ...outerStyles },
|
|
560
|
+
});
|
|
532
561
|
buffer = "";
|
|
533
562
|
};
|
|
563
|
+
const wrap = (inner, add) => {
|
|
564
|
+
pushPlain();
|
|
565
|
+
const nested = parseInlineSegments(inner, { ...outerStyles, ...add });
|
|
566
|
+
result.push(...nested);
|
|
567
|
+
};
|
|
534
568
|
let i = 0;
|
|
535
569
|
while (i < cleaned.length) {
|
|
570
|
+
if (cleaned.startsWith("***", i)) {
|
|
571
|
+
const end = cleaned.indexOf("***", i + 3);
|
|
572
|
+
if (end !== -1) {
|
|
573
|
+
wrap(cleaned.slice(i + 3, end), { bold: true, italic: true });
|
|
574
|
+
i = end + 3;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (cleaned.startsWith("___", i)) {
|
|
579
|
+
const end = cleaned.indexOf("___", i + 3);
|
|
580
|
+
if (end !== -1) {
|
|
581
|
+
wrap(cleaned.slice(i + 3, end), { bold: true, italic: true });
|
|
582
|
+
i = end + 3;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
536
586
|
if (cleaned.startsWith("**", i)) {
|
|
537
587
|
const end = cleaned.indexOf("**", i + 2);
|
|
538
588
|
if (end !== -1) {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
589
|
+
wrap(cleaned.slice(i + 2, end), { bold: true });
|
|
590
|
+
i = end + 2;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (cleaned.startsWith("__", i)) {
|
|
595
|
+
const end = cleaned.indexOf("__", i + 2);
|
|
596
|
+
if (end !== -1) {
|
|
597
|
+
wrap(cleaned.slice(i + 2, end), { bold: true });
|
|
546
598
|
i = end + 2;
|
|
547
599
|
continue;
|
|
548
600
|
}
|
|
@@ -550,13 +602,7 @@ function parseInlineMarkdown(text) {
|
|
|
550
602
|
if (cleaned.startsWith("~~", i)) {
|
|
551
603
|
const end = cleaned.indexOf("~~", i + 2);
|
|
552
604
|
if (end !== -1) {
|
|
553
|
-
|
|
554
|
-
const inner = cleaned.slice(i + 2, end);
|
|
555
|
-
result.push({
|
|
556
|
-
type: "text",
|
|
557
|
-
text: unescapeMarkdown(inner),
|
|
558
|
-
styles: { strike: true },
|
|
559
|
-
});
|
|
605
|
+
wrap(cleaned.slice(i + 2, end), { strike: true });
|
|
560
606
|
i = end + 2;
|
|
561
607
|
continue;
|
|
562
608
|
}
|
|
@@ -569,7 +615,7 @@ function parseInlineMarkdown(text) {
|
|
|
569
615
|
result.push({
|
|
570
616
|
type: "text",
|
|
571
617
|
text: unescapeMarkdown(inner),
|
|
572
|
-
styles: { code: true },
|
|
618
|
+
styles: { ...outerStyles, code: true },
|
|
573
619
|
});
|
|
574
620
|
i = end + 1;
|
|
575
621
|
continue;
|
|
@@ -583,7 +629,7 @@ function parseInlineMarkdown(text) {
|
|
|
583
629
|
pushPlain();
|
|
584
630
|
const label = cleaned.slice(i + 1, endLabel);
|
|
585
631
|
const href = cleaned.slice(startLink + 1, endLink);
|
|
586
|
-
const parsedLabel =
|
|
632
|
+
const parsedLabel = parseInlineSegments(label, {});
|
|
587
633
|
// Ensure link content is never undefined - if empty, add empty text
|
|
588
634
|
const linkContent = parsedLabel.length > 0 ? parsedLabel : [{ type: "text", text: "", styles: {} }];
|
|
589
635
|
result.push({
|
|
@@ -597,15 +643,9 @@ function parseInlineMarkdown(text) {
|
|
|
597
643
|
}
|
|
598
644
|
if (cleaned[i] === "*" || cleaned[i] === "_") {
|
|
599
645
|
const marker = cleaned[i];
|
|
600
|
-
const end = cleaned
|
|
646
|
+
const end = findItalicClose(cleaned, i + 1, marker);
|
|
601
647
|
if (end !== -1) {
|
|
602
|
-
|
|
603
|
-
const inner = cleaned.slice(i + 1, end);
|
|
604
|
-
result.push({
|
|
605
|
-
type: "text",
|
|
606
|
-
text: unescapeMarkdown(inner),
|
|
607
|
-
styles: { italic: true },
|
|
608
|
-
});
|
|
648
|
+
wrap(cleaned.slice(i + 1, end), { italic: true });
|
|
609
649
|
i = end + 1;
|
|
610
650
|
continue;
|
|
611
651
|
}
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -384,7 +384,13 @@ function App() {
|
|
|
384
384
|
schema: customSchema,
|
|
385
385
|
pasteHandler: createMarkdownPasteHandler(markdownToBlocks),
|
|
386
386
|
uploadFile: async (file: File) => {
|
|
387
|
-
|
|
387
|
+
const url = `https://placehold.co/600x400?text=${encodeURIComponent(file.name)}`;
|
|
388
|
+
return {
|
|
389
|
+
props: {
|
|
390
|
+
url,
|
|
391
|
+
name: file.name,
|
|
392
|
+
},
|
|
393
|
+
};
|
|
388
394
|
},
|
|
389
395
|
});
|
|
390
396
|
const [markdown, setMarkdown] = useState("");
|
|
@@ -474,8 +480,14 @@ function App() {
|
|
|
474
480
|
if (typeof result === "string") {
|
|
475
481
|
return { url: result };
|
|
476
482
|
}
|
|
477
|
-
if (result && typeof result === "object"
|
|
478
|
-
|
|
483
|
+
if (result && typeof result === "object") {
|
|
484
|
+
if ("url" in result && typeof (result as any).url === "string") {
|
|
485
|
+
return { url: (result as any).url as string };
|
|
486
|
+
}
|
|
487
|
+
const propsUrl = (result as any).props?.url;
|
|
488
|
+
if (typeof propsUrl === "string") {
|
|
489
|
+
return { url: propsUrl };
|
|
490
|
+
}
|
|
479
491
|
}
|
|
480
492
|
throw new Error("uploadFile did not return a URL");
|
|
481
493
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+?)(?:\s+=\d+x(?:\d+|\*))?\)/g;
|
|
2
2
|
const MARKDOWN_ESCAPE_REGEX = /([*_\\])/g;
|
|
3
|
-
const INLINE_SEGMENT_REGEX =
|
|
4
|
-
/(\*\*\*[^*]+\*\*\*|___[^_]+___|\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|<u>[^<]+<\/u>)/;
|
|
5
3
|
|
|
6
4
|
export function escapeHtml(text: string): string {
|
|
7
5
|
return text
|
|
@@ -25,53 +23,125 @@ function restoreEscapes(text: string): string {
|
|
|
25
23
|
return text.replace(/\uE000/g, "\\");
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
function findItalicClose(
|
|
27
|
+
text: string,
|
|
28
|
+
start: number,
|
|
29
|
+
marker: "*" | "_",
|
|
30
|
+
): number {
|
|
31
|
+
let j = start;
|
|
32
|
+
while (j < text.length) {
|
|
33
|
+
const ch = text[j];
|
|
34
|
+
if (ch === "\uE000") {
|
|
35
|
+
j += 2;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if ((ch === "*" || ch === "_") && text[j + 1] === ch) {
|
|
39
|
+
const close = text.indexOf(ch + ch, j + 2);
|
|
40
|
+
if (close === -1) {
|
|
41
|
+
return -1;
|
|
42
|
+
}
|
|
43
|
+
j = close + 2;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ch === marker) {
|
|
47
|
+
return j;
|
|
48
|
+
}
|
|
49
|
+
j += 1;
|
|
31
50
|
}
|
|
51
|
+
return -1;
|
|
52
|
+
}
|
|
32
53
|
|
|
33
|
-
|
|
34
|
-
|
|
54
|
+
function parseInlineSegments(
|
|
55
|
+
normalized: string,
|
|
56
|
+
outer: { bold: boolean; italic: boolean; underline: boolean },
|
|
57
|
+
): InlineSegment[] {
|
|
58
|
+
const result: InlineSegment[] = [];
|
|
59
|
+
let buffer = "";
|
|
60
|
+
|
|
61
|
+
const pushPlain = () => {
|
|
62
|
+
if (!buffer) return;
|
|
63
|
+
result.push({ text: restoreEscapes(buffer), styles: { ...outer } });
|
|
64
|
+
buffer = "";
|
|
65
|
+
};
|
|
35
66
|
|
|
36
|
-
|
|
37
|
-
|
|
67
|
+
const wrap = (
|
|
68
|
+
inner: string,
|
|
69
|
+
add: Partial<{ bold: boolean; italic: boolean; underline: boolean }>,
|
|
70
|
+
) => {
|
|
71
|
+
pushPlain();
|
|
72
|
+
result.push(...parseInlineSegments(inner, { ...outer, ...add }));
|
|
73
|
+
};
|
|
38
74
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
75
|
+
let i = 0;
|
|
76
|
+
while (i < normalized.length) {
|
|
77
|
+
if (normalized.startsWith("***", i)) {
|
|
78
|
+
const end = normalized.indexOf("***", i + 3);
|
|
79
|
+
if (end !== -1) {
|
|
80
|
+
wrap(normalized.slice(i + 3, end), { bold: true, italic: true });
|
|
81
|
+
i = end + 3;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
45
84
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
85
|
+
if (normalized.startsWith("___", i)) {
|
|
86
|
+
const end = normalized.indexOf("___", i + 3);
|
|
87
|
+
if (end !== -1) {
|
|
88
|
+
wrap(normalized.slice(i + 3, end), { bold: true, italic: true });
|
|
89
|
+
i = end + 3;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
53
92
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
93
|
+
if (normalized.startsWith("**", i)) {
|
|
94
|
+
const end = normalized.indexOf("**", i + 2);
|
|
95
|
+
if (end !== -1) {
|
|
96
|
+
wrap(normalized.slice(i + 2, end), { bold: true });
|
|
97
|
+
i = end + 2;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
61
100
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
101
|
+
if (normalized.startsWith("__", i)) {
|
|
102
|
+
const end = normalized.indexOf("__", i + 2);
|
|
103
|
+
if (end !== -1) {
|
|
104
|
+
wrap(normalized.slice(i + 2, end), { bold: true });
|
|
105
|
+
i = end + 2;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (normalized.startsWith("<u>", i)) {
|
|
110
|
+
const end = normalized.indexOf("</u>", i + 3);
|
|
111
|
+
if (end !== -1) {
|
|
112
|
+
wrap(normalized.slice(i + 3, end), { underline: true });
|
|
113
|
+
i = end + 4;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
69
116
|
}
|
|
117
|
+
if (normalized[i] === "*" || normalized[i] === "_") {
|
|
118
|
+
const marker = normalized[i] as "*" | "_";
|
|
119
|
+
const end = findItalicClose(normalized, i + 1, marker);
|
|
120
|
+
if (end !== -1) {
|
|
121
|
+
wrap(normalized.slice(i + 1, end), { italic: true });
|
|
122
|
+
i = end + 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
buffer += normalized[i];
|
|
128
|
+
i += 1;
|
|
129
|
+
}
|
|
70
130
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
131
|
+
pushPlain();
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseInlineMarkdown(text: string): InlineSegment[] {
|
|
136
|
+
if (!text) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const normalized = text.replace(/\\([*_`~])/g, "\uE000$1");
|
|
141
|
+
return parseInlineSegments(normalized, {
|
|
142
|
+
bold: false,
|
|
143
|
+
italic: false,
|
|
144
|
+
underline: false,
|
|
75
145
|
});
|
|
76
146
|
}
|
|
77
147
|
|
|
@@ -512,6 +512,7 @@ export const stepBlock = createReactBlockSpec(
|
|
|
512
512
|
onChange={handleStepTitleChange}
|
|
513
513
|
autoFocus={stepTitle.length === 0}
|
|
514
514
|
multiline
|
|
515
|
+
disableNewlines
|
|
515
516
|
enableAutocomplete
|
|
516
517
|
fieldName="title"
|
|
517
518
|
suggestionFilter={(suggestion) => (suggestion as StepSuggestion).isSnippet !== true}
|
|
@@ -25,6 +25,7 @@ type StepFieldProps = {
|
|
|
25
25
|
autoFocus?: boolean;
|
|
26
26
|
focusSignal?: number;
|
|
27
27
|
multiline?: boolean;
|
|
28
|
+
disableNewlines?: boolean;
|
|
28
29
|
enableAutocomplete?: boolean;
|
|
29
30
|
fieldName?: string;
|
|
30
31
|
suggestionFilter?: (suggestion: Suggestion) => boolean;
|
|
@@ -671,6 +672,7 @@ export function StepField({
|
|
|
671
672
|
autoFocus,
|
|
672
673
|
focusSignal,
|
|
673
674
|
multiline = false,
|
|
675
|
+
disableNewlines = false,
|
|
674
676
|
enableAutocomplete = false,
|
|
675
677
|
fieldName,
|
|
676
678
|
suggestionFilter,
|
|
@@ -945,6 +947,15 @@ export function StepField({
|
|
|
945
947
|
textareaNode.focus();
|
|
946
948
|
}, [focusSignal, textareaNode]);
|
|
947
949
|
|
|
950
|
+
useAutoResize({
|
|
951
|
+
textarea: textareaNode,
|
|
952
|
+
enabled: multiline,
|
|
953
|
+
onResize: useCallback(() => {
|
|
954
|
+
const instance = editorInstanceRef.current as (OverTypeInstance & { _updateAutoHeight?: () => void }) | null;
|
|
955
|
+
instance?._updateAutoHeight?.();
|
|
956
|
+
}, []),
|
|
957
|
+
});
|
|
958
|
+
|
|
948
959
|
useEffect(() => {
|
|
949
960
|
const instance = editorInstanceRef.current;
|
|
950
961
|
if (!instance) {
|
|
@@ -1012,13 +1023,6 @@ export function StepField({
|
|
|
1012
1023
|
textareaNode.readOnly = readOnly;
|
|
1013
1024
|
}, [readOnly, textareaNode]);
|
|
1014
1025
|
|
|
1015
|
-
useAutoResize({
|
|
1016
|
-
textarea: textareaNode,
|
|
1017
|
-
multiline,
|
|
1018
|
-
minRows: 3,
|
|
1019
|
-
maxRows: 16,
|
|
1020
|
-
});
|
|
1021
|
-
|
|
1022
1026
|
useEffect(() => {
|
|
1023
1027
|
if (!textareaNode) {
|
|
1024
1028
|
return;
|
|
@@ -1697,6 +1701,12 @@ export function StepField({
|
|
|
1697
1701
|
return;
|
|
1698
1702
|
}
|
|
1699
1703
|
|
|
1704
|
+
if (disableNewlines && (event.key === "Enter" || event.code === "Enter")) {
|
|
1705
|
+
event.preventDefault();
|
|
1706
|
+
event.stopImmediatePropagation();
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1700
1710
|
if (event.key === "Tab") {
|
|
1701
1711
|
const moved = focusAdjacentField(event.shiftKey ? -1 : 1);
|
|
1702
1712
|
if (moved) {
|
|
@@ -1705,7 +1715,7 @@ export function StepField({
|
|
|
1705
1715
|
}
|
|
1706
1716
|
}
|
|
1707
1717
|
};
|
|
1708
|
-
}, [activeSuggestionIndex, applySuggestion, enableAutocomplete, filteredSuggestions, focusAdjacentField, handleToolbarAction, readOnly, shouldShowAutocomplete]);
|
|
1718
|
+
}, [activeSuggestionIndex, applySuggestion, disableNewlines, enableAutocomplete, filteredSuggestions, focusAdjacentField, handleToolbarAction, readOnly, shouldShowAutocomplete]);
|
|
1709
1719
|
|
|
1710
1720
|
useEffect(() => {
|
|
1711
1721
|
if (!textareaNode) {
|
|
@@ -1,58 +1,68 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
|
|
3
3
|
type Options = {
|
|
4
4
|
textarea: HTMLTextAreaElement | null;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
maxRows?: number;
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
onResize: () => void;
|
|
8
7
|
};
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (!textarea || !multiline) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
9
|
+
// Shared across all hook instances: one fonts.ready promise per document,
|
|
10
|
+
// fanned out to every registered callback so N fields cost 1 .then().
|
|
11
|
+
const fontsReadyCallbacks = new Set<() => void>();
|
|
12
|
+
let fontsReadyAttached = false;
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
14
|
+
function registerFontsReady(cb: () => void): () => void {
|
|
15
|
+
if (typeof document === "undefined" || !document.fonts?.ready) {
|
|
16
|
+
return () => {};
|
|
17
|
+
}
|
|
18
|
+
fontsReadyCallbacks.add(cb);
|
|
19
|
+
if (!fontsReadyAttached) {
|
|
20
|
+
fontsReadyAttached = true;
|
|
21
|
+
document.fonts.ready
|
|
22
|
+
.then(() => {
|
|
23
|
+
for (const fn of fontsReadyCallbacks) fn();
|
|
24
|
+
})
|
|
25
|
+
.catch(() => {});
|
|
26
|
+
}
|
|
27
|
+
return () => {
|
|
28
|
+
fontsReadyCallbacks.delete(cb);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
export function useAutoResize({ textarea, enabled = true, onResize }: Options) {
|
|
33
|
+
const frameRef = useRef<number>(0);
|
|
34
|
+
const onResizeRef = useRef(onResize);
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
};
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
onResizeRef.current = onResize;
|
|
38
|
+
}, [onResize]);
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!textarea || !enabled) return;
|
|
41
42
|
|
|
42
43
|
let cancelled = false;
|
|
43
|
-
|
|
44
|
+
|
|
45
|
+
// All callers go through this coalescing scheduler: repeated pings in a
|
|
46
|
+
// single frame collapse to one reflow.
|
|
47
|
+
const schedule = () => {
|
|
48
|
+
if (cancelled) return;
|
|
49
|
+
if (frameRef.current) return;
|
|
44
50
|
frameRef.current = requestAnimationFrame(() => {
|
|
45
|
-
|
|
51
|
+
frameRef.current = 0;
|
|
52
|
+
if (!cancelled) onResizeRef.current();
|
|
46
53
|
});
|
|
47
|
-
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Initial pass after layout.
|
|
57
|
+
schedule();
|
|
48
58
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
59
|
+
// One-shot: re-run once the textarea actually enters the layout tree.
|
|
60
|
+
// This is the piece that fixes the drag-drop remount and
|
|
61
|
+
// snippet-insert-while-hidden cases OverType itself cannot recover from.
|
|
52
62
|
const intersectionObserver = new IntersectionObserver((entries) => {
|
|
53
63
|
for (const entry of entries) {
|
|
54
|
-
if (entry.isIntersecting
|
|
55
|
-
|
|
64
|
+
if (entry.isIntersecting) {
|
|
65
|
+
schedule();
|
|
56
66
|
intersectionObserver.disconnect();
|
|
57
67
|
break;
|
|
58
68
|
}
|
|
@@ -60,20 +70,20 @@ export function useAutoResize({ textarea, multiline = false, minRows = 2, maxRow
|
|
|
60
70
|
});
|
|
61
71
|
intersectionObserver.observe(textarea);
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
document.fonts.ready.then(() => {
|
|
65
|
-
if (!cancelled) resize();
|
|
66
|
-
}).catch(() => {});
|
|
67
|
-
}
|
|
73
|
+
const unregisterFonts = registerFontsReady(schedule);
|
|
68
74
|
|
|
69
75
|
return () => {
|
|
70
76
|
cancelled = true;
|
|
71
|
-
mutationObserver.disconnect();
|
|
72
|
-
resizeObserver.disconnect();
|
|
73
77
|
intersectionObserver.disconnect();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
unregisterFonts();
|
|
79
|
+
if (frameRef.current) {
|
|
80
|
+
cancelAnimationFrame(frameRef.current);
|
|
81
|
+
frameRef.current = 0;
|
|
82
|
+
}
|
|
77
83
|
};
|
|
78
|
-
}, [textarea,
|
|
84
|
+
}, [textarea, enabled]);
|
|
85
|
+
|
|
86
|
+
return useCallback(() => {
|
|
87
|
+
onResizeRef.current();
|
|
88
|
+
}, []);
|
|
79
89
|
}
|
|
@@ -630,7 +630,41 @@ export function blocksToMarkdown(blocks: CustomEditorBlock[]): string {
|
|
|
630
630
|
}
|
|
631
631
|
|
|
632
632
|
function parseInlineMarkdown(text: string): EditorInline[] {
|
|
633
|
-
|
|
633
|
+
return parseInlineSegments(stripHtmlWrappers(text), {});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function findItalicClose(
|
|
637
|
+
text: string,
|
|
638
|
+
start: number,
|
|
639
|
+
marker: "*" | "_",
|
|
640
|
+
): number {
|
|
641
|
+
let j = start;
|
|
642
|
+
while (j < text.length) {
|
|
643
|
+
const ch = text[j];
|
|
644
|
+
if (ch === "\\") {
|
|
645
|
+
j += 2;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if ((ch === "*" || ch === "_") && text[j + 1] === ch) {
|
|
649
|
+
const close = text.indexOf(ch + ch, j + 2);
|
|
650
|
+
if (close === -1) {
|
|
651
|
+
return -1;
|
|
652
|
+
}
|
|
653
|
+
j = close + 2;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (ch === marker) {
|
|
657
|
+
return j;
|
|
658
|
+
}
|
|
659
|
+
j += 1;
|
|
660
|
+
}
|
|
661
|
+
return -1;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function parseInlineSegments(
|
|
665
|
+
cleaned: string,
|
|
666
|
+
outerStyles: Record<string, boolean>,
|
|
667
|
+
): EditorInline[] {
|
|
634
668
|
const result: EditorInline[] = [];
|
|
635
669
|
let buffer = "";
|
|
636
670
|
|
|
@@ -638,22 +672,53 @@ function parseInlineMarkdown(text: string): EditorInline[] {
|
|
|
638
672
|
if (buffer.length === 0) {
|
|
639
673
|
return;
|
|
640
674
|
}
|
|
641
|
-
result.push({
|
|
675
|
+
result.push({
|
|
676
|
+
type: "text",
|
|
677
|
+
text: unescapeMarkdown(buffer),
|
|
678
|
+
styles: { ...outerStyles } as EditorStyles,
|
|
679
|
+
});
|
|
642
680
|
buffer = "";
|
|
643
681
|
};
|
|
644
682
|
|
|
683
|
+
const wrap = (inner: string, add: Record<string, boolean>) => {
|
|
684
|
+
pushPlain();
|
|
685
|
+
const nested = parseInlineSegments(inner, { ...outerStyles, ...add });
|
|
686
|
+
result.push(...nested);
|
|
687
|
+
};
|
|
688
|
+
|
|
645
689
|
let i = 0;
|
|
646
690
|
while (i < cleaned.length) {
|
|
691
|
+
if (cleaned.startsWith("***", i)) {
|
|
692
|
+
const end = cleaned.indexOf("***", i + 3);
|
|
693
|
+
if (end !== -1) {
|
|
694
|
+
wrap(cleaned.slice(i + 3, end), { bold: true, italic: true });
|
|
695
|
+
i = end + 3;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (cleaned.startsWith("___", i)) {
|
|
701
|
+
const end = cleaned.indexOf("___", i + 3);
|
|
702
|
+
if (end !== -1) {
|
|
703
|
+
wrap(cleaned.slice(i + 3, end), { bold: true, italic: true });
|
|
704
|
+
i = end + 3;
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
647
709
|
if (cleaned.startsWith("**", i)) {
|
|
648
710
|
const end = cleaned.indexOf("**", i + 2);
|
|
649
711
|
if (end !== -1) {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
712
|
+
wrap(cleaned.slice(i + 2, end), { bold: true });
|
|
713
|
+
i = end + 2;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (cleaned.startsWith("__", i)) {
|
|
719
|
+
const end = cleaned.indexOf("__", i + 2);
|
|
720
|
+
if (end !== -1) {
|
|
721
|
+
wrap(cleaned.slice(i + 2, end), { bold: true });
|
|
657
722
|
i = end + 2;
|
|
658
723
|
continue;
|
|
659
724
|
}
|
|
@@ -662,13 +727,7 @@ function parseInlineMarkdown(text: string): EditorInline[] {
|
|
|
662
727
|
if (cleaned.startsWith("~~", i)) {
|
|
663
728
|
const end = cleaned.indexOf("~~", i + 2);
|
|
664
729
|
if (end !== -1) {
|
|
665
|
-
|
|
666
|
-
const inner = cleaned.slice(i + 2, end);
|
|
667
|
-
result.push({
|
|
668
|
-
type: "text",
|
|
669
|
-
text: unescapeMarkdown(inner),
|
|
670
|
-
styles: { strike: true },
|
|
671
|
-
});
|
|
730
|
+
wrap(cleaned.slice(i + 2, end), { strike: true });
|
|
672
731
|
i = end + 2;
|
|
673
732
|
continue;
|
|
674
733
|
}
|
|
@@ -682,7 +741,7 @@ function parseInlineMarkdown(text: string): EditorInline[] {
|
|
|
682
741
|
result.push({
|
|
683
742
|
type: "text",
|
|
684
743
|
text: unescapeMarkdown(inner),
|
|
685
|
-
styles: { code: true },
|
|
744
|
+
styles: { ...outerStyles, code: true } as EditorStyles,
|
|
686
745
|
});
|
|
687
746
|
i = end + 1;
|
|
688
747
|
continue;
|
|
@@ -697,7 +756,7 @@ function parseInlineMarkdown(text: string): EditorInline[] {
|
|
|
697
756
|
pushPlain();
|
|
698
757
|
const label = cleaned.slice(i + 1, endLabel);
|
|
699
758
|
const href = cleaned.slice(startLink + 1, endLink);
|
|
700
|
-
const parsedLabel =
|
|
759
|
+
const parsedLabel = parseInlineSegments(label, {});
|
|
701
760
|
// Ensure link content is never undefined - if empty, add empty text
|
|
702
761
|
const linkContent = parsedLabel.length > 0 ? parsedLabel : [{ type: "text", text: "", styles: {} }];
|
|
703
762
|
result.push({
|
|
@@ -711,16 +770,10 @@ function parseInlineMarkdown(text: string): EditorInline[] {
|
|
|
711
770
|
}
|
|
712
771
|
|
|
713
772
|
if (cleaned[i] === "*" || cleaned[i] === "_") {
|
|
714
|
-
const marker = cleaned[i];
|
|
715
|
-
const end = cleaned
|
|
773
|
+
const marker = cleaned[i] as "*" | "_";
|
|
774
|
+
const end = findItalicClose(cleaned, i + 1, marker);
|
|
716
775
|
if (end !== -1) {
|
|
717
|
-
|
|
718
|
-
const inner = cleaned.slice(i + 1, end);
|
|
719
|
-
result.push({
|
|
720
|
-
type: "text",
|
|
721
|
-
text: unescapeMarkdown(inner),
|
|
722
|
-
styles: { italic: true },
|
|
723
|
-
});
|
|
776
|
+
wrap(cleaned.slice(i + 1, end), { italic: true });
|
|
724
777
|
i = end + 1;
|
|
725
778
|
continue;
|
|
726
779
|
}
|
|
@@ -130,4 +130,43 @@ describe("markdownToBlocks", () => {
|
|
|
130
130
|
children: [],
|
|
131
131
|
});
|
|
132
132
|
});
|
|
133
|
+
|
|
134
|
+
it("parses combined bold+italic using nested delimiters", () => {
|
|
135
|
+
const blocks = markdownToBlocks(
|
|
136
|
+
"The _**Username**_ and **_Password_** fields and ***both*** and ___both___.",
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(blocks).toHaveLength(1);
|
|
140
|
+
expect(blocks[0]).toEqual({
|
|
141
|
+
type: "paragraph",
|
|
142
|
+
props: baseProps,
|
|
143
|
+
content: [
|
|
144
|
+
{ type: "text", text: "The ", styles: {} },
|
|
145
|
+
{ type: "text", text: "Username", styles: { italic: true, bold: true } },
|
|
146
|
+
{ type: "text", text: " and ", styles: {} },
|
|
147
|
+
{ type: "text", text: "Password", styles: { bold: true, italic: true } },
|
|
148
|
+
{ type: "text", text: " fields and ", styles: {} },
|
|
149
|
+
{ type: "text", text: "both", styles: { bold: true, italic: true } },
|
|
150
|
+
{ type: "text", text: " and ", styles: {} },
|
|
151
|
+
{ type: "text", text: "both", styles: { bold: true, italic: true } },
|
|
152
|
+
{ type: "text", text: ".", styles: {} },
|
|
153
|
+
],
|
|
154
|
+
children: [],
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("parses bold with nested italic keeping both styles", () => {
|
|
159
|
+
const blocks = markdownToBlocks("**foo _bar_ baz**");
|
|
160
|
+
|
|
161
|
+
expect(blocks[0]).toEqual({
|
|
162
|
+
type: "paragraph",
|
|
163
|
+
props: baseProps,
|
|
164
|
+
content: [
|
|
165
|
+
{ type: "text", text: "foo ", styles: { bold: true } },
|
|
166
|
+
{ type: "text", text: "bar", styles: { bold: true, italic: true } },
|
|
167
|
+
{ type: "text", text: " baz", styles: { bold: true } },
|
|
168
|
+
],
|
|
169
|
+
children: [],
|
|
170
|
+
});
|
|
171
|
+
});
|
|
133
172
|
});
|