@colixsystems/widget-sdk 0.275.2 → 0.276.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 -3
- package/dist/contract.cjs +8 -5
- package/dist/contract.js +8 -5
- package/dist/index.d.ts +13 -13
- package/dist/markdown-doc.js +209 -0
- package/dist/markdown-editor-bridge.js +162 -0
- package/dist/markdown-editor-core.js +184 -0
- package/dist/markdown-editor-css.js +50 -0
- package/dist/markdown-editor-field.js +111 -0
- package/dist/markdown-editor-field.native.js +9 -0
- package/dist/markdown-editor-html.js +4 -0
- package/dist/markdown-editor-webview.js +70 -0
- package/dist/markdown-input-view.js +43 -101
- package/dist/markdown-input.js +4 -6
- package/dist/markdown-input.native.js +3 -5
- package/dist/markdown.js +1 -112
- package/dist/richtext-tokens.js +1 -25
- package/package.json +62 -2
- package/src/contract.js +8 -5
- package/dist/markdown-edit.js +0 -164
- package/dist/markdown-live-field.js +0 -312
- package/dist/markdown-live-field.native.js +0 -36
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// sc-15080 — the page `<MarkdownInput>` runs its editor in on iOS and Android.
|
|
2
|
+
//
|
|
3
|
+
// React Native has no editable rich-text surface, so the native field hosts
|
|
4
|
+
// the SAME editor module the web field mounts, inside a WebView. This entry is
|
|
5
|
+
// bundled into ./markdown-editor-html.js by scripts/build-markdown-editor.mjs;
|
|
6
|
+
// nothing imports it at runtime.
|
|
7
|
+
//
|
|
8
|
+
// Messages are JSON strings. In: init, style, set, command, focus. Out:
|
|
9
|
+
// ready, change, height, focus, selection, state.
|
|
10
|
+
|
|
11
|
+
import { createMarkdownEditor } from "./markdown-editor-core.js";
|
|
12
|
+
import { markdownEditorCss } from "./markdown-editor-css.js";
|
|
13
|
+
|
|
14
|
+
const ROOT = "#root";
|
|
15
|
+
|
|
16
|
+
function post(message) {
|
|
17
|
+
if (window.ReactNativeWebView) window.ReactNativeWebView.postMessage(JSON.stringify(message));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let instance = null;
|
|
21
|
+
const style = document.createElement("style");
|
|
22
|
+
document.head.appendChild(style);
|
|
23
|
+
|
|
24
|
+
function paint({ tokens, minHeight }) {
|
|
25
|
+
if (tokens && typeof tokens === "object") style.textContent = markdownEditorCss(tokens, ROOT, { minHeight });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function init(message) {
|
|
29
|
+
if (instance) return;
|
|
30
|
+
const { markdown, placeholder, label } = message;
|
|
31
|
+
paint(message);
|
|
32
|
+
|
|
33
|
+
const root = document.querySelector(ROOT);
|
|
34
|
+
instance = createMarkdownEditor({
|
|
35
|
+
element: root,
|
|
36
|
+
markdown,
|
|
37
|
+
placeholder,
|
|
38
|
+
label,
|
|
39
|
+
onChange: (next) => post({ type: "change", markdown: next }),
|
|
40
|
+
onFocusChange: (focused) => post({ type: "focus", focused }),
|
|
41
|
+
onState: (state) => post({ type: "state", inTable: state.inTable === true }),
|
|
42
|
+
});
|
|
43
|
+
instance.editor.on("selectionUpdate", () => post({ type: "selection", text: instance.selectedText() }));
|
|
44
|
+
|
|
45
|
+
let lastHeight = 0;
|
|
46
|
+
new ResizeObserver(() => {
|
|
47
|
+
const height = Math.ceil(root.getBoundingClientRect().height);
|
|
48
|
+
if (height !== lastHeight) post({ type: "height", height: (lastHeight = height) });
|
|
49
|
+
}).observe(root);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
window.mdEditor = {
|
|
53
|
+
receive(raw) {
|
|
54
|
+
let message;
|
|
55
|
+
try {
|
|
56
|
+
message = JSON.parse(raw);
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!message || typeof message !== "object") return;
|
|
61
|
+
if (message.type === "init") init(message);
|
|
62
|
+
else if (message.type === "style") paint(message);
|
|
63
|
+
else if (!instance) return;
|
|
64
|
+
else if (message.type === "set") instance.setMarkdown(message.markdown);
|
|
65
|
+
else if (message.type === "command") instance.command(message.name, message.args);
|
|
66
|
+
else if (message.type === "focus") instance.focus();
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
post({ type: "ready" });
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// sc-6970 — the ONE implementation behind the `<MarkdownInput>` SDK primitive.
|
|
2
2
|
//
|
|
3
|
-
// The authoring half of `<RichText
|
|
4
|
-
// author's SELECTION in markdown markers. Widgets previously had only a bare
|
|
3
|
+
// The authoring half of `<RichText>`. Widgets previously had only a bare
|
|
5
4
|
// `TextInput`, so a widget asked for a formatting toolbar had to splice literal
|
|
6
5
|
// `<strong>`/`<br>` tags into the text — markup the author then read as tag soup.
|
|
7
6
|
//
|
|
8
|
-
// sc-
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// sc-15080: the author edits the formatted result directly — headings, lists,
|
|
8
|
+
// links and real tables — while the stored value stays markdown. Each host
|
|
9
|
+
// binding supplies its own `EditorField` around the one shared editor (mounted
|
|
10
|
+
// in the page on web, in a WebView natively), so `showPreview` has nothing
|
|
11
|
+
// left to show and is accepted as a no-op.
|
|
12
12
|
//
|
|
13
13
|
// The two platform bindings differ ONLY in where the primitives come from; the
|
|
14
14
|
// component is defined once here so the hosts cannot drift (CLAUDE.md §3, §8).
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
import React from "react";
|
|
17
17
|
import { useHostTheme } from "./hooks.js";
|
|
18
18
|
import { resolveRichTextTokens } from "./richtext-tokens.js";
|
|
19
|
-
import { markdownEditOps } from "./markdown-edit.js";
|
|
20
19
|
import { makeMarkdownLinkDialog } from "./markdown-link-dialog.js";
|
|
21
20
|
|
|
22
21
|
const TOOLS = Object.freeze([
|
|
@@ -28,8 +27,7 @@ const TOOLS = Object.freeze([
|
|
|
28
27
|
{ key: "bullet", label: "•", accessibilityLabel: "Bulleted list" },
|
|
29
28
|
{ key: "ordered", label: "1.", accessibilityLabel: "Numbered list" },
|
|
30
29
|
{ key: "table", label: "Table", accessibilityLabel: "Insert table" },
|
|
31
|
-
// sc-7349:
|
|
32
|
-
// so this button opens the form instead of editing the string directly.
|
|
30
|
+
// sc-7349: a link needs a target no selection supplies, so this opens a form.
|
|
33
31
|
{
|
|
34
32
|
key: "link",
|
|
35
33
|
label: "Link",
|
|
@@ -38,17 +36,26 @@ const TOOLS = Object.freeze([
|
|
|
38
36
|
},
|
|
39
37
|
]);
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
// Shown while the caret is in a table: a phone keyboard has no Tab key to
|
|
40
|
+
// grow one, and a table must be removable without selecting around it.
|
|
41
|
+
const TABLE_TOOLS = Object.freeze([
|
|
42
|
+
{ key: "addRow", label: "+ Row", accessibilityLabel: "Add row" },
|
|
43
|
+
{ key: "addColumn", label: "+ Column", accessibilityLabel: "Add column" },
|
|
44
|
+
{ key: "removeRow", label: "− Row", accessibilityLabel: "Remove row" },
|
|
45
|
+
{ key: "removeColumn", label: "− Column", accessibilityLabel: "Remove column" },
|
|
46
|
+
{ key: "removeTable", label: "Delete table", accessibilityLabel: "Delete table" },
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
export function makeMarkdownInput(rn, Modal, Button, EditorField) {
|
|
42
50
|
const { Text, View, Pressable } = rn;
|
|
43
51
|
const MarkdownLinkDialog = makeMarkdownLinkDialog(rn, Modal, Button);
|
|
44
52
|
|
|
53
|
+
// `previewLabel`, `showPreview` and `renderImage` are accepted for widgets
|
|
54
|
+
// written before the field showed the formatted result itself.
|
|
45
55
|
function MarkdownInput({
|
|
46
56
|
value,
|
|
47
57
|
onChange,
|
|
48
58
|
placeholder,
|
|
49
|
-
previewLabel = "Preview",
|
|
50
|
-
showPreview = false,
|
|
51
|
-
renderImage,
|
|
52
59
|
pages,
|
|
53
60
|
minHeight = 150,
|
|
54
61
|
maxHeight = 280,
|
|
@@ -58,72 +65,34 @@ export function makeMarkdownInput(rn, RichText, Modal, Button, LiveField) {
|
|
|
58
65
|
}) {
|
|
59
66
|
const theme = useHostTheme();
|
|
60
67
|
const tokens = React.useMemo(() => resolveRichTextTokens(theme), [theme]);
|
|
61
|
-
const [selection, setSelection] = React.useState({ start: 0, end: 0 });
|
|
62
68
|
const fieldRef = React.useRef(null);
|
|
63
|
-
const [
|
|
64
|
-
|
|
65
|
-
const
|
|
69
|
+
const [linkLabel, setLinkLabel] = React.useState(null);
|
|
70
|
+
const [inTable, setInTable] = React.useState(false);
|
|
71
|
+
const onStateChange = React.useCallback((state) => setInTable(state.inTable), []);
|
|
66
72
|
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
73
|
+
const run = React.useCallback((name, args) => {
|
|
74
|
+
const field = fieldRef.current;
|
|
75
|
+
if (field) field.command(name, args);
|
|
70
76
|
}, []);
|
|
71
77
|
|
|
72
|
-
const applyTool = React.useCallback(
|
|
73
|
-
(toolKey, options) => {
|
|
74
|
-
if (typeof onChange !== "function") return;
|
|
75
|
-
const edit = markdownEditOps[toolKey];
|
|
76
|
-
if (!edit) return;
|
|
77
|
-
const next = edit(text, selection, options);
|
|
78
|
-
const field = fieldRef.current;
|
|
79
|
-
if (field && typeof field.recordEdit === "function") field.recordEdit();
|
|
80
|
-
onChange(next.value);
|
|
81
|
-
setSelection(next.selection);
|
|
82
|
-
},
|
|
83
|
-
[onChange, text, selection],
|
|
84
|
-
);
|
|
85
|
-
|
|
86
78
|
const pressTool = React.useCallback(
|
|
87
79
|
(tool) => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
applyTool(tool.key);
|
|
80
|
+
// The selection seeds the form's Link text, so a highlighted phrase
|
|
81
|
+
// stays the label.
|
|
82
|
+
if (tool.opensDialog) setLinkLabel(fieldRef.current ? fieldRef.current.selectedText() : "");
|
|
83
|
+
else run(tool.key);
|
|
93
84
|
},
|
|
94
|
-
[
|
|
85
|
+
[run],
|
|
95
86
|
);
|
|
96
87
|
|
|
97
88
|
const insertLink = React.useCallback(
|
|
98
89
|
(result) => {
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
setLinkLabel(null);
|
|
91
|
+
run("link", result);
|
|
101
92
|
},
|
|
102
|
-
[
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
// The selection seeds the dialog's Link text, so highlighting a phrase and
|
|
106
|
-
// pressing Link keeps that phrase as the label.
|
|
107
|
-
const selected = text.slice(
|
|
108
|
-
Math.min(selection.start, selection.end),
|
|
109
|
-
Math.max(selection.start, selection.end),
|
|
93
|
+
[run],
|
|
110
94
|
);
|
|
111
95
|
|
|
112
|
-
const fieldStyle = {
|
|
113
|
-
fontFamily: tokens.bodyFont,
|
|
114
|
-
fontSize: tokens.bodySize,
|
|
115
|
-
lineHeight: tokens.lineHeight,
|
|
116
|
-
color: tokens.color,
|
|
117
|
-
backgroundColor: tokens.surface,
|
|
118
|
-
borderWidth: 1,
|
|
119
|
-
borderColor: tokens.border,
|
|
120
|
-
borderRadius: tokens.radius,
|
|
121
|
-
padding: tokens.padding,
|
|
122
|
-
minHeight,
|
|
123
|
-
maxHeight,
|
|
124
|
-
textAlignVertical: "top",
|
|
125
|
-
};
|
|
126
|
-
|
|
127
96
|
return React.createElement(
|
|
128
97
|
View,
|
|
129
98
|
{ style, testID },
|
|
@@ -137,7 +106,7 @@ export function makeMarkdownInput(rn, RichText, Modal, Button, LiveField) {
|
|
|
137
106
|
},
|
|
138
107
|
testID: testID ? `${testID}-toolbar` : undefined,
|
|
139
108
|
},
|
|
140
|
-
TOOLS.map((tool) =>
|
|
109
|
+
(inTable ? [...TOOLS, ...TABLE_TOOLS] : TOOLS).map((tool) =>
|
|
141
110
|
React.createElement(
|
|
142
111
|
Pressable,
|
|
143
112
|
{
|
|
@@ -174,51 +143,24 @@ export function makeMarkdownInput(rn, RichText, Modal, Button, LiveField) {
|
|
|
174
143
|
),
|
|
175
144
|
),
|
|
176
145
|
),
|
|
177
|
-
React.createElement(
|
|
146
|
+
React.createElement(EditorField, {
|
|
178
147
|
ref: fieldRef,
|
|
179
|
-
value:
|
|
148
|
+
value: typeof value === "string" ? value : "",
|
|
180
149
|
onChangeText: onChange,
|
|
181
|
-
|
|
182
|
-
selection,
|
|
150
|
+
onStateChange,
|
|
183
151
|
placeholder,
|
|
184
152
|
accessibilityLabel,
|
|
185
153
|
tokens,
|
|
186
|
-
|
|
154
|
+
minHeight,
|
|
155
|
+
maxHeight,
|
|
187
156
|
testID: testID ? `${testID}-field` : undefined,
|
|
188
157
|
}),
|
|
189
|
-
showPreview && text.trim()
|
|
190
|
-
? React.createElement(
|
|
191
|
-
View,
|
|
192
|
-
{ style: { marginTop: tokens.padding } },
|
|
193
|
-
React.createElement(
|
|
194
|
-
Text,
|
|
195
|
-
{
|
|
196
|
-
style: {
|
|
197
|
-
fontFamily: tokens.bodyFont,
|
|
198
|
-
fontSize: tokens.codeSize,
|
|
199
|
-
color: tokens.mutedColor,
|
|
200
|
-
marginBottom: tokens.blockGap,
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
previewLabel,
|
|
204
|
-
),
|
|
205
|
-
React.createElement(RichText, {
|
|
206
|
-
value: text,
|
|
207
|
-
renderImage,
|
|
208
|
-
// sc-7349: the preview shows the reader's document, but a tap
|
|
209
|
-
// must NOT leave the page — that would throw away the draft the
|
|
210
|
-
// author is still writing.
|
|
211
|
-
followLinks: false,
|
|
212
|
-
testID: testID ? `${testID}-preview` : undefined,
|
|
213
|
-
}),
|
|
214
|
-
)
|
|
215
|
-
: null,
|
|
216
158
|
React.createElement(MarkdownLinkDialog, {
|
|
217
|
-
visible:
|
|
218
|
-
onClose: () =>
|
|
159
|
+
visible: linkLabel !== null,
|
|
160
|
+
onClose: () => setLinkLabel(null),
|
|
219
161
|
onSubmit: insertLink,
|
|
220
162
|
pages,
|
|
221
|
-
initialLabel:
|
|
163
|
+
initialLabel: linkLabel || "",
|
|
222
164
|
testID,
|
|
223
165
|
}),
|
|
224
166
|
);
|
package/dist/markdown-input.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
// sc-6970 — `<MarkdownInput>` (web). Binds the shared implementation in
|
|
2
|
-
// ./markdown-input-view.js to react-native-web's primitives
|
|
3
|
-
//
|
|
2
|
+
// ./markdown-input-view.js to react-native-web's primitives and the web
|
|
3
|
+
// editor field.
|
|
4
4
|
|
|
5
5
|
import { ReactNative } from "./text-scale.js";
|
|
6
6
|
import { makeMarkdownInput } from "./markdown-input-view.js";
|
|
7
|
-
import {
|
|
8
|
-
import { RichText } from "./richtext.js";
|
|
7
|
+
import { MarkdownEditorField } from "./markdown-editor-field.js";
|
|
9
8
|
import { Modal } from "./modal.js";
|
|
10
9
|
import { Button } from "./button.js";
|
|
11
10
|
import { TextInput } from "./primitives.js";
|
|
@@ -13,8 +12,7 @@ import { TextInput } from "./primitives.js";
|
|
|
13
12
|
// TextInput stays for the link form, which is built from the same `rn`.
|
|
14
13
|
export const MarkdownInput = makeMarkdownInput(
|
|
15
14
|
{ ...ReactNative, TextInput },
|
|
16
|
-
RichText,
|
|
17
15
|
Modal,
|
|
18
16
|
Button,
|
|
19
|
-
|
|
17
|
+
MarkdownEditorField,
|
|
20
18
|
);
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// sc-6970 — `<MarkdownInput>` (native). Binds the shared implementation in
|
|
2
2
|
// ./markdown-input-view.js to React Native's own primitives and the native
|
|
3
|
-
//
|
|
3
|
+
// editor field. Only the imports differ from the web mirror.
|
|
4
4
|
|
|
5
5
|
import { View, Pressable, ScrollView } from "react-native";
|
|
6
6
|
import { Text, TextInput } from "./bounded-text.native.js";
|
|
7
7
|
import { makeMarkdownInput } from "./markdown-input-view.js";
|
|
8
|
-
import {
|
|
9
|
-
import { RichText } from "./richtext.native.js";
|
|
8
|
+
import { MarkdownEditorField } from "./markdown-editor-field.native.js";
|
|
10
9
|
import { Modal } from "./modal.native.js";
|
|
11
10
|
import { Button } from "./button.native.js";
|
|
12
11
|
|
|
@@ -14,8 +13,7 @@ import { Button } from "./button.native.js";
|
|
|
14
13
|
// scroll rather than grow an app's every page past the panel.
|
|
15
14
|
export const MarkdownInput = makeMarkdownInput(
|
|
16
15
|
{ Text, View, Pressable, TextInput, ScrollView },
|
|
17
|
-
RichText,
|
|
18
16
|
Modal,
|
|
19
17
|
Button,
|
|
20
|
-
|
|
18
|
+
MarkdownEditorField,
|
|
21
19
|
);
|
package/dist/markdown.js
CHANGED
|
@@ -196,8 +196,7 @@ const PLAIN_PATTERN =
|
|
|
196
196
|
* Splits one line into positioned tokens: `{ type: "text", start, end }`, or
|
|
197
197
|
* `{ type: "bold"|"italic"|"code", start, end, contentStart, contentEnd }`, or
|
|
198
198
|
* `{ type: "link", start, end, contentStart, contentEnd, href }` whose content
|
|
199
|
-
* is the RAW label.
|
|
200
|
-
* editor cannot style a construct the renderer would not draw.
|
|
199
|
+
* is the RAW label.
|
|
201
200
|
*/
|
|
202
201
|
function scanInline(source) {
|
|
203
202
|
const tokens = [];
|
|
@@ -532,116 +531,6 @@ export function parseMarkdown(text, options) {
|
|
|
532
531
|
return blocks;
|
|
533
532
|
}
|
|
534
533
|
|
|
535
|
-
const LIVE_STYLE_KEYS = ["marker", "bold", "italic", "code", "href", "heading"];
|
|
536
|
-
|
|
537
|
-
function sameLiveStyle(a, b) {
|
|
538
|
-
return LIVE_STYLE_KEYS.every((key) => (a[key] || false) === (b[key] || false));
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
* sc-14974 — `text` cut into styled runs for the live editor. Joining every
|
|
543
|
-
* run's `text` gives back `text` byte for byte: nothing is trimmed or joined,
|
|
544
|
-
* and markdown markers stay in place flagged `marker` so the editor can dim
|
|
545
|
-
* them. Each run is `{ text, marker?, bold?, italic?, code?, href?, heading? }`
|
|
546
|
-
* with `heading` the level. Lines are classified in `parseMarkdown`'s order.
|
|
547
|
-
*/
|
|
548
|
-
export function markdownLiveSegments(text) {
|
|
549
|
-
const source = typeof text === "string" ? text : "";
|
|
550
|
-
const runs = [];
|
|
551
|
-
const push = (value, style) => {
|
|
552
|
-
if (!value) return;
|
|
553
|
-
const last = runs[runs.length - 1];
|
|
554
|
-
if (last && sameLiveStyle(last, style)) last.text += value;
|
|
555
|
-
else runs.push({ text: value, ...style });
|
|
556
|
-
};
|
|
557
|
-
const inline = (value, base) => {
|
|
558
|
-
for (const token of scanInline(value)) {
|
|
559
|
-
if (token.type === "text") {
|
|
560
|
-
push(value.slice(token.start, token.end), base);
|
|
561
|
-
continue;
|
|
562
|
-
}
|
|
563
|
-
const content = value.slice(token.contentStart, token.contentEnd);
|
|
564
|
-
push(value.slice(token.start, token.contentStart), { ...base, marker: true });
|
|
565
|
-
if (token.type === "link") inline(content, { ...base, href: token.href });
|
|
566
|
-
else push(content, { ...base, [token.type]: true });
|
|
567
|
-
push(value.slice(token.contentEnd, token.end), { ...base, marker: true });
|
|
568
|
-
}
|
|
569
|
-
};
|
|
570
|
-
const tableRow = (line, base) => {
|
|
571
|
-
let cellStart = 0;
|
|
572
|
-
for (let k = 0; k < line.length; k += 1) {
|
|
573
|
-
if (line.charAt(k) === "\\" && line.charAt(k + 1) === "|") k += 1;
|
|
574
|
-
else if (line.charAt(k) === "|") {
|
|
575
|
-
inline(line.slice(cellStart, k), base);
|
|
576
|
-
push("|", { marker: true });
|
|
577
|
-
cellStart = k + 1;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
inline(line.slice(cellStart), base);
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
const lines = source.split("\n");
|
|
584
|
-
let inFence = false;
|
|
585
|
-
let tableStart = -1;
|
|
586
|
-
let tableEnd = -1;
|
|
587
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
588
|
-
if (i > 0) push("\n", {});
|
|
589
|
-
const line = lines[i];
|
|
590
|
-
const trimmed = line.trimEnd();
|
|
591
|
-
const tail = line.slice(trimmed.length);
|
|
592
|
-
|
|
593
|
-
if (inFence) {
|
|
594
|
-
if (FENCE_CLOSE_RE.test(line)) inFence = false;
|
|
595
|
-
push(line, inFence ? { code: true } : { marker: true });
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
if (FENCE_OPEN_RE.test(trimmed)) {
|
|
599
|
-
inFence = true;
|
|
600
|
-
push(line, { marker: true });
|
|
601
|
-
continue;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
if (i > tableEnd && TABLE_ROW_RE.test(line) && TABLE_DIVIDER_RE.test(lines[i + 1] || "")) {
|
|
605
|
-
tableStart = i;
|
|
606
|
-
tableEnd = i + 1;
|
|
607
|
-
while (tableEnd + 1 < lines.length && TABLE_ROW_RE.test(lines[tableEnd + 1])) tableEnd += 1;
|
|
608
|
-
}
|
|
609
|
-
if (i <= tableEnd) {
|
|
610
|
-
if (i === tableStart + 1) push(line, { marker: true });
|
|
611
|
-
else tableRow(line, i === tableStart ? { bold: true } : {});
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const lead = trimmed.length - trimmed.trimStart().length;
|
|
616
|
-
const image = IMAGE_RE.exec(trimmed.trimStart());
|
|
617
|
-
if (image) {
|
|
618
|
-
const altStart = lead + 2;
|
|
619
|
-
const altEnd = altStart + image[1].length;
|
|
620
|
-
push(line.slice(0, altStart), { marker: true });
|
|
621
|
-
push(line.slice(altStart, altEnd), {});
|
|
622
|
-
push(line.slice(altEnd, trimmed.length), { marker: true });
|
|
623
|
-
push(tail, {});
|
|
624
|
-
continue;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
const heading = HEADING_RE.exec(trimmed);
|
|
628
|
-
const listItem = heading ? null : BULLET_RE.exec(trimmed) || ORDERED_RE.exec(trimmed);
|
|
629
|
-
if (heading || listItem) {
|
|
630
|
-
const content = heading ? heading[2] : listItem[listItem.length - 1];
|
|
631
|
-
const base = heading ? { heading: heading[1].length } : {};
|
|
632
|
-
const contentStart = trimmed.length - content.length;
|
|
633
|
-
push(trimmed.slice(0, contentStart), { ...base, marker: true });
|
|
634
|
-
inline(content, base);
|
|
635
|
-
push(tail, base);
|
|
636
|
-
continue;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
inline(trimmed, {});
|
|
640
|
-
push(tail, {});
|
|
641
|
-
}
|
|
642
|
-
return runs;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
534
|
function spansToText(spans) {
|
|
646
535
|
return spans.map((span) => span.text).join("");
|
|
647
536
|
}
|
package/dist/richtext-tokens.js
CHANGED
|
@@ -58,7 +58,7 @@ export function resolveRichTextTokens(theme) {
|
|
|
58
58
|
|
|
59
59
|
export { IMAGE_WIDTH as RICH_TEXT_IMAGE_WIDTH };
|
|
60
60
|
|
|
61
|
-
/** The style one inline span is drawn with
|
|
61
|
+
/** The style one inline span is drawn with by `<RichText>`. */
|
|
62
62
|
export function richTextSpanStyle(span, tokens) {
|
|
63
63
|
return {
|
|
64
64
|
...(span.bold ? { fontWeight: "700" } : null),
|
|
@@ -71,27 +71,3 @@ export function richTextSpanStyle(span, tokens) {
|
|
|
71
71
|
...(span.href ? { color: tokens.accent, textDecorationLine: "underline" } : null),
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* sc-14974 — the style one `markdownLiveSegments` run is drawn with in
|
|
77
|
-
* `<MarkdownInput>`: the span's `<RichText>` look, sized as its heading, with
|
|
78
|
-
* markers dimmed so the text reads as the result while staying editable.
|
|
79
|
-
*/
|
|
80
|
-
export function markdownLiveRunStyle(run, tokens) {
|
|
81
|
-
const headingSize = run.heading
|
|
82
|
-
? tokens.headingSizes[run.heading] || tokens.headingSizes[3]
|
|
83
|
-
: null;
|
|
84
|
-
return {
|
|
85
|
-
...(headingSize
|
|
86
|
-
? {
|
|
87
|
-
fontFamily: tokens.headingFont,
|
|
88
|
-
fontWeight: "700",
|
|
89
|
-
fontSize: headingSize,
|
|
90
|
-
// The field's body leading would overlap a larger heading line.
|
|
91
|
-
lineHeight: Math.round(headingSize * 1.3),
|
|
92
|
-
}
|
|
93
|
-
: null),
|
|
94
|
-
...richTextSpanStyle(run, tokens),
|
|
95
|
-
...(run.marker ? { color: tokens.mutedColor, textDecorationLine: "none" } : null),
|
|
96
|
-
};
|
|
97
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.276.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|
|
@@ -75,7 +75,22 @@
|
|
|
75
75
|
"react": ">=18.0.0",
|
|
76
76
|
"react-native": "*",
|
|
77
77
|
"react-dom": ">=18.0.0",
|
|
78
|
-
"react-native-web": ">=0.19.0"
|
|
78
|
+
"react-native-web": ">=0.19.0",
|
|
79
|
+
"@tiptap/core": "^3.32.2",
|
|
80
|
+
"@tiptap/extension-bold": "^3.32.2",
|
|
81
|
+
"@tiptap/extension-code": "^3.32.2",
|
|
82
|
+
"@tiptap/extension-code-block": "^3.32.2",
|
|
83
|
+
"@tiptap/extension-document": "^3.32.2",
|
|
84
|
+
"@tiptap/extension-heading": "^3.32.2",
|
|
85
|
+
"@tiptap/extension-image": "^3.32.2",
|
|
86
|
+
"@tiptap/extension-italic": "^3.32.2",
|
|
87
|
+
"@tiptap/extension-link": "^3.32.2",
|
|
88
|
+
"@tiptap/extension-list": "^3.32.2",
|
|
89
|
+
"@tiptap/extension-paragraph": "^3.32.2",
|
|
90
|
+
"@tiptap/extension-table": "^3.32.2",
|
|
91
|
+
"@tiptap/extension-text": "^3.32.2",
|
|
92
|
+
"@tiptap/extensions": "^3.32.2",
|
|
93
|
+
"react-native-webview": ">=13.7.0"
|
|
79
94
|
},
|
|
80
95
|
"peerDependenciesMeta": {
|
|
81
96
|
"react": {
|
|
@@ -89,6 +104,51 @@
|
|
|
89
104
|
},
|
|
90
105
|
"react-dom": {
|
|
91
106
|
"optional": true
|
|
107
|
+
},
|
|
108
|
+
"@tiptap/core": {
|
|
109
|
+
"optional": true
|
|
110
|
+
},
|
|
111
|
+
"@tiptap/extension-bold": {
|
|
112
|
+
"optional": true
|
|
113
|
+
},
|
|
114
|
+
"@tiptap/extension-code": {
|
|
115
|
+
"optional": true
|
|
116
|
+
},
|
|
117
|
+
"@tiptap/extension-code-block": {
|
|
118
|
+
"optional": true
|
|
119
|
+
},
|
|
120
|
+
"@tiptap/extension-document": {
|
|
121
|
+
"optional": true
|
|
122
|
+
},
|
|
123
|
+
"@tiptap/extension-heading": {
|
|
124
|
+
"optional": true
|
|
125
|
+
},
|
|
126
|
+
"@tiptap/extension-image": {
|
|
127
|
+
"optional": true
|
|
128
|
+
},
|
|
129
|
+
"@tiptap/extension-italic": {
|
|
130
|
+
"optional": true
|
|
131
|
+
},
|
|
132
|
+
"@tiptap/extension-link": {
|
|
133
|
+
"optional": true
|
|
134
|
+
},
|
|
135
|
+
"@tiptap/extension-list": {
|
|
136
|
+
"optional": true
|
|
137
|
+
},
|
|
138
|
+
"@tiptap/extension-paragraph": {
|
|
139
|
+
"optional": true
|
|
140
|
+
},
|
|
141
|
+
"@tiptap/extension-table": {
|
|
142
|
+
"optional": true
|
|
143
|
+
},
|
|
144
|
+
"@tiptap/extension-text": {
|
|
145
|
+
"optional": true
|
|
146
|
+
},
|
|
147
|
+
"@tiptap/extensions": {
|
|
148
|
+
"optional": true
|
|
149
|
+
},
|
|
150
|
+
"react-native-webview": {
|
|
151
|
+
"optional": true
|
|
92
152
|
}
|
|
93
153
|
},
|
|
94
154
|
"optionalDependencies": {
|
package/src/contract.js
CHANGED
|
@@ -3464,13 +3464,13 @@ const PRIMITIVES = [
|
|
|
3464
3464
|
rnComponent: null,
|
|
3465
3465
|
docsUrl: null,
|
|
3466
3466
|
},
|
|
3467
|
-
// sc-6970 — formatted content, authored. The authoring half of `<RichText
|
|
3468
|
-
// a
|
|
3469
|
-
//
|
|
3467
|
+
// sc-6970 — formatted content, authored. The authoring half of `<RichText>`,
|
|
3468
|
+
// so a generated widget never has to splice HTML tags into a plain TextInput
|
|
3469
|
+
// again. sc-15080: the field edits the formatted result directly.
|
|
3470
3470
|
{
|
|
3471
3471
|
name: "MarkdownInput",
|
|
3472
3472
|
description:
|
|
3473
|
-
|
|
3473
|
+
"Formatted-content editor. `<MarkdownInput value={draft} onChange={setDraft} />`. The default way to let an app USER write formatted text: a rich-text field where the user edits the FORMATTED RESULT directly — headings, bold, italic, code, links, bulleted and numbered lists and real tables are shown as they will read, with no markdown markers in view — plus a toolbar (bold, italic, code, H2, H3, bullets, numbered list), a Table button that inserts a real table, with buttons to add or remove rows and columns or delete the table while the caret is in one, and a Link button that opens a form for the link text plus its target. Typing `## `, `- `, `1. `, `**bold**` or `*italic*` formats as you type. Use it rather than a bare `TextInput` plus formatting buttons of your own, and do NOT add a `<RichText>` preview under it: the field already shows the result; an editor you do build (one that only bolds, a checklist editor) still stores markdown markers and previews with `<RichText>` — buttons that splice `<strong>`/`<br>`/`<table>` tags into the string leave the author reading tag soup. The stored value is markdown text in the `<RichText>` grammar; loading a value never rewrites it, and an edit writes each construct in its canonical spelling. On iOS and Android the field runs in a WebView bundled into the SDK (no network). Props: `value`, `onChange(next)`, `placeholder`, `pages` (optional `[{ id, name }]` — supply it and the Link form offers those pages to link to by name; without it the form offers only an external address, because a widget cannot know the app's pages on its own), `minHeight` (default 150) and `maxHeight` (default 280) — the field grows with its content between them, then scrolls — `accessibilityLabel`, `style`, `testID`. `showPreview`, `previewLabel` and `renderImage` are still accepted and ignored. Images show inside the field when their source is an http(s) URL and as a labelled placeholder when it is a filestore id. Links in the field are inert, so a tap while editing cannot navigate away from the draft.",
|
|
3474
3474
|
rnComponent: null,
|
|
3475
3475
|
docsUrl: null,
|
|
3476
3476
|
},
|
|
@@ -6570,7 +6570,10 @@ const CONTRACT = deepFreeze({
|
|
|
6570
6570
|
// ships a hand-written `sv` value beside `en`.
|
|
6571
6571
|
// 1.225.0: behaviour (sc-14974) — `<MarkdownInput>` styles its own field
|
|
6572
6572
|
// live; `showPreview` now defaults to false.
|
|
6573
|
-
|
|
6573
|
+
// 1.226.0: behaviour (sc-15080) — `<MarkdownInput>` is a rich-text editor
|
|
6574
|
+
// showing the formatted result; `showPreview`, `previewLabel` and
|
|
6575
|
+
// `renderImage` are accepted and ignored.
|
|
6576
|
+
version: "1.226.0",
|
|
6574
6577
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6575
6578
|
hooks: HOOKS,
|
|
6576
6579
|
primitives: PRIMITIVES,
|