@eclosion-tech/react-native-yjs-text 0.1.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/CHANGELOG.md +99 -0
- package/LICENSE +21 -0
- package/README.md +323 -0
- package/SPEC.md +346 -0
- package/android/build.gradle +26 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/tech/eclosion/yjstext/YjsTextModule.kt +77 -0
- package/android/src/main/java/tech/eclosion/yjstext/YjsTextSupport.kt +135 -0
- package/android/src/main/java/tech/eclosion/yjstext/YjsTextView.kt +424 -0
- package/build/YTextInput.d.ts +23 -0
- package/build/YTextInput.d.ts.map +1 -0
- package/build/YTextInput.js +178 -0
- package/build/YTextInput.js.map +1 -0
- package/build/YTextRenderer.d.ts +15 -0
- package/build/YTextRenderer.d.ts.map +1 -0
- package/build/YTextRenderer.js +85 -0
- package/build/YTextRenderer.js.map +1 -0
- package/build/bridge.d.ts +88 -0
- package/build/bridge.d.ts.map +1 -0
- package/build/bridge.js +231 -0
- package/build/bridge.js.map +1 -0
- package/build/index.d.ts +13 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +12 -0
- package/build/index.js.map +1 -0
- package/build/internal/NativeYTextInputView.d.ts +114 -0
- package/build/internal/NativeYTextInputView.d.ts.map +1 -0
- package/build/internal/NativeYTextInputView.js +27 -0
- package/build/internal/NativeYTextInputView.js.map +1 -0
- package/build/internal/editorRegistry.d.ts +23 -0
- package/build/internal/editorRegistry.d.ts.map +1 -0
- package/build/internal/editorRegistry.js +26 -0
- package/build/internal/editorRegistry.js.map +1 -0
- package/build/schema.d.ts +51 -0
- package/build/schema.d.ts.map +1 -0
- package/build/schema.js +134 -0
- package/build/schema.js.map +1 -0
- package/build/types.d.ts +182 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +11 -0
- package/build/types.js.map +1 -0
- package/build/useYTextEditor.d.ts +21 -0
- package/build/useYTextEditor.d.ts.map +1 -0
- package/build/useYTextEditor.js +166 -0
- package/build/useYTextEditor.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/YjsText.podspec +30 -0
- package/ios/YjsTextModule.swift +75 -0
- package/ios/YjsTextSupport.swift +135 -0
- package/ios/YjsTextView.swift +464 -0
- package/package.json +124 -0
- package/src/YTextInput.tsx +263 -0
- package/src/YTextRenderer.tsx +96 -0
- package/src/bridge.ts +283 -0
- package/src/index.ts +21 -0
- package/src/internal/NativeYTextInputView.tsx +126 -0
- package/src/internal/editorRegistry.ts +50 -0
- package/src/schema.ts +157 -0
- package/src/types.ts +194 -0
- package/src/useYTextEditor.ts +171 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { YTextRendererProps } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Read-only rich-text renderer for a Y.Text.
|
|
5
|
+
*
|
|
6
|
+
* Implemented as nested `<Text>` views — no native `UITextView` /
|
|
7
|
+
* `AppCompatEditText` instance, no event handling, no keyboard. Significantly
|
|
8
|
+
* cheaper than {@link YTextInput} and the right choice for lists of read-only
|
|
9
|
+
* content (feeds, notification bodies, message previews, etc.).
|
|
10
|
+
*
|
|
11
|
+
* Observes the Y.Text and re-renders on every change. The consumer should
|
|
12
|
+
* unmount this component when the underlying Y.Text is freed.
|
|
13
|
+
*/
|
|
14
|
+
export declare function YTextRenderer(props: YTextRendererProps): React.ReactElement;
|
|
15
|
+
//# sourceMappingURL=YTextRenderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"YTextRenderer.d.ts","sourceRoot":"","sources":["../src/YTextRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EAA0B,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAE1E;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,KAAK,CAAC,YAAY,CA6B3E"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Text } from 'react-native';
|
|
3
|
+
import { ytextToRuns } from './bridge';
|
|
4
|
+
/**
|
|
5
|
+
* Read-only rich-text renderer for a Y.Text.
|
|
6
|
+
*
|
|
7
|
+
* Implemented as nested `<Text>` views — no native `UITextView` /
|
|
8
|
+
* `AppCompatEditText` instance, no event handling, no keyboard. Significantly
|
|
9
|
+
* cheaper than {@link YTextInput} and the right choice for lists of read-only
|
|
10
|
+
* content (feeds, notification bodies, message previews, etc.).
|
|
11
|
+
*
|
|
12
|
+
* Observes the Y.Text and re-renders on every change. The consumer should
|
|
13
|
+
* unmount this component when the underlying Y.Text is freed.
|
|
14
|
+
*/
|
|
15
|
+
export function YTextRenderer(props) {
|
|
16
|
+
const { yText, schema, style, numberOfLines } = props;
|
|
17
|
+
const [version, setVersion] = React.useState(0);
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
function onChange() {
|
|
20
|
+
setVersion((v) => v + 1);
|
|
21
|
+
}
|
|
22
|
+
yText.observe(onChange);
|
|
23
|
+
return () => {
|
|
24
|
+
yText.unobserve(onChange);
|
|
25
|
+
};
|
|
26
|
+
}, [yText]);
|
|
27
|
+
const runs = React.useMemo(() => ytextToRuns(yText), [yText, version]);
|
|
28
|
+
// Empty Y.Text — render an empty Text rather than nothing, so the consumer's
|
|
29
|
+
// layout (line height, spacing) is preserved.
|
|
30
|
+
if (runs.length === 0) {
|
|
31
|
+
return <Text style={style} numberOfLines={numberOfLines}/>;
|
|
32
|
+
}
|
|
33
|
+
return (<Text style={style} numberOfLines={numberOfLines}>
|
|
34
|
+
{runs.map((run, index) => (<RunText key={index} run={run} schema={schema}/>))}
|
|
35
|
+
</Text>);
|
|
36
|
+
}
|
|
37
|
+
function RunText({ run, schema }) {
|
|
38
|
+
// Layer each mark's renderStyle in declaration order; consumer-defined
|
|
39
|
+
// marks later in the schema map override earlier ones if they collide —
|
|
40
|
+
// EXCEPT for `textDecorationLine`, which RN's style-flatten would just
|
|
41
|
+
// overwrite (last write wins). Multiple marks can legitimately want both
|
|
42
|
+
// decorations at once (e.g. `underline` + `strike`), and RN accepts the
|
|
43
|
+
// combined token `"underline line-through"` for both. We accumulate the
|
|
44
|
+
// distinct decoration tokens across marks and apply the merged value as
|
|
45
|
+
// the final style layer.
|
|
46
|
+
const styles = [];
|
|
47
|
+
const decorationTokens = new Set();
|
|
48
|
+
let onPress;
|
|
49
|
+
for (const markName of Object.keys(run.marks)) {
|
|
50
|
+
const value = run.marks[markName];
|
|
51
|
+
// `null` / `false` are the "explicitly off" sentinels produced by the
|
|
52
|
+
// insert path's `computeInsertAttrs`. Skip them so we don't re-apply
|
|
53
|
+
// the mark's renderStyle to text the user just toggled it off for.
|
|
54
|
+
if (value === null || value === false || value === undefined)
|
|
55
|
+
continue;
|
|
56
|
+
const spec = schema.marks[markName];
|
|
57
|
+
if (!spec)
|
|
58
|
+
continue;
|
|
59
|
+
if (spec.renderStyle) {
|
|
60
|
+
const { textDecorationLine, ...rest } = spec.renderStyle;
|
|
61
|
+
if (textDecorationLine && textDecorationLine !== 'none') {
|
|
62
|
+
for (const token of String(textDecorationLine).split(/\s+/)) {
|
|
63
|
+
if (token === 'underline' || token === 'line-through') {
|
|
64
|
+
decorationTokens.add(token);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
styles.push(rest);
|
|
69
|
+
}
|
|
70
|
+
if (spec.onTap) {
|
|
71
|
+
const attrs = run.marks[markName] ?? {};
|
|
72
|
+
onPress = () => spec.onTap?.(attrs);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (decorationTokens.size > 0) {
|
|
76
|
+
// Stable order: underline first, line-through second. RN accepts
|
|
77
|
+
// "underline line-through" / "none" / each on its own.
|
|
78
|
+
const ordered = ['underline', 'line-through'].filter((t) => decorationTokens.has(t));
|
|
79
|
+
styles.push({ textDecorationLine: ordered.join(' ') });
|
|
80
|
+
}
|
|
81
|
+
return (<Text style={styles} onPress={onPress}>
|
|
82
|
+
{run.text}
|
|
83
|
+
</Text>);
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=YTextRenderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"YTextRenderer.js","sourceRoot":"","sources":["../src/YTextRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,IAAI,EAAkB,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAGvC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,KAAyB;IACrD,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,KAAK,CAAC;IACtD,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAEhD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,SAAS,QAAQ;YACf,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3B,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACxB,OAAO,GAAG,EAAE;YACV,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC5B,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAEZ,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAQ,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;IAE9E,6EAA6E;IAC7E,8CAA8C;IAC9C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC,CAAC,aAAa,CAAC,EAAG,CAAC;IAC9D,CAAC;IAED,OAAO,CACL,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC,CAAC,aAAa,CAAC,CAC/C;MAAA,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,CACxB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,EAAG,CAClD,CAAC,CACJ;IAAA,EAAE,IAAI,CAAC,CACR,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAgC;IAC5D,uEAAuE;IACvE,wEAAwE;IACxE,uEAAuE;IACvE,yEAAyE;IACzE,wEAAwE;IACxE,wEAAwE;IACxE,wEAAwE;IACxE,yBAAyB;IACzB,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAC;IAC3C,IAAI,OAAiC,CAAC;IACtC,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9C,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAClC,sEAAsE;QACtE,qEAAqE;QACrE,mEAAmE;QACnE,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QACvE,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACpC,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,MAAM,EAAE,kBAAkB,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC;YACzD,IAAI,kBAAkB,IAAI,kBAAkB,KAAK,MAAM,EAAE,CAAC;gBACxD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,kBAAkB,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;oBAC5D,IAAI,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,cAAc,EAAE,CAAC;wBACtD,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;oBAC9B,CAAC;gBACH,CAAC;YACH,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpB,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,MAAM,KAAK,GAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAe,IAAI,EAAE,CAAC;YACvD,OAAO,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IACD,IAAI,gBAAgB,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QAC9B,iEAAiE;QACjE,uDAAuD;QACvD,MAAM,OAAO,GAAG,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACrF,MAAM,CAAC,IAAI,CAAC,EAAE,kBAAkB,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAoC,EAAE,CAAC,CAAC;IAC5F,CAAC;IACD,OAAO,CACL,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CACpC;MAAA,CAAC,GAAG,CAAC,IAAI,CACX;IAAA,EAAE,IAAI,CAAC,CACR,CAAC;AACJ,CAAC","sourcesContent":["import * as React from 'react';\nimport { Text, type TextStyle } from 'react-native';\n\nimport { ytextToRuns } from './bridge';\nimport type { MarkAttrs, Run, Schema, YTextRendererProps } from './types';\n\n/**\n * Read-only rich-text renderer for a Y.Text.\n *\n * Implemented as nested `<Text>` views — no native `UITextView` /\n * `AppCompatEditText` instance, no event handling, no keyboard. Significantly\n * cheaper than {@link YTextInput} and the right choice for lists of read-only\n * content (feeds, notification bodies, message previews, etc.).\n *\n * Observes the Y.Text and re-renders on every change. The consumer should\n * unmount this component when the underlying Y.Text is freed.\n */\nexport function YTextRenderer(props: YTextRendererProps): React.ReactElement {\n const { yText, schema, style, numberOfLines } = props;\n const [version, setVersion] = React.useState(0);\n\n React.useEffect(() => {\n function onChange(): void {\n setVersion((v) => v + 1);\n }\n yText.observe(onChange);\n return () => {\n yText.unobserve(onChange);\n };\n }, [yText]);\n\n const runs = React.useMemo<Run[]>(() => ytextToRuns(yText), [yText, version]);\n\n // Empty Y.Text — render an empty Text rather than nothing, so the consumer's\n // layout (line height, spacing) is preserved.\n if (runs.length === 0) {\n return <Text style={style} numberOfLines={numberOfLines} />;\n }\n\n return (\n <Text style={style} numberOfLines={numberOfLines}>\n {runs.map((run, index) => (\n <RunText key={index} run={run} schema={schema} />\n ))}\n </Text>\n );\n}\n\nfunction RunText({ run, schema }: { run: Run; schema: Schema }): React.ReactElement {\n // Layer each mark's renderStyle in declaration order; consumer-defined\n // marks later in the schema map override earlier ones if they collide —\n // EXCEPT for `textDecorationLine`, which RN's style-flatten would just\n // overwrite (last write wins). Multiple marks can legitimately want both\n // decorations at once (e.g. `underline` + `strike`), and RN accepts the\n // combined token `\"underline line-through\"` for both. We accumulate the\n // distinct decoration tokens across marks and apply the merged value as\n // the final style layer.\n const styles: TextStyle[] = [];\n const decorationTokens = new Set<string>();\n let onPress: (() => void) | undefined;\n for (const markName of Object.keys(run.marks)) {\n const value = run.marks[markName];\n // `null` / `false` are the \"explicitly off\" sentinels produced by the\n // insert path's `computeInsertAttrs`. Skip them so we don't re-apply\n // the mark's renderStyle to text the user just toggled it off for.\n if (value === null || value === false || value === undefined) continue;\n const spec = schema.marks[markName];\n if (!spec) continue;\n if (spec.renderStyle) {\n const { textDecorationLine, ...rest } = spec.renderStyle;\n if (textDecorationLine && textDecorationLine !== 'none') {\n for (const token of String(textDecorationLine).split(/\\s+/)) {\n if (token === 'underline' || token === 'line-through') {\n decorationTokens.add(token);\n }\n }\n }\n styles.push(rest);\n }\n if (spec.onTap) {\n const attrs = (run.marks[markName] as MarkAttrs) ?? {};\n onPress = () => spec.onTap?.(attrs);\n }\n }\n if (decorationTokens.size > 0) {\n // Stable order: underline first, line-through second. RN accepts\n // \"underline line-through\" / \"none\" / each on its own.\n const ordered = ['underline', 'line-through'].filter((t) => decorationTokens.has(t));\n styles.push({ textDecorationLine: ordered.join(' ') as TextStyle['textDecorationLine'] });\n }\n return (\n <Text style={styles} onPress={onPress}>\n {run.text}\n </Text>\n );\n}\n"]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
import type { MarkAttrs, ReplaceEdit, Run, Schema, SelectionRange } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Convert a Y.Text into the array of attributed runs sent down to the native
|
|
5
|
+
* view as a prop.
|
|
6
|
+
*
|
|
7
|
+
* v0.1 only deals with string inserts — embeds (the future inline-non-text node
|
|
8
|
+
* story documented in the spec) are deferred. If the underlying delta contains
|
|
9
|
+
* a non-string insert we stringify defensively rather than throwing.
|
|
10
|
+
*/
|
|
11
|
+
export declare function ytextToRuns(yText: Y.Text): Run[];
|
|
12
|
+
/**
|
|
13
|
+
* Compute the marks that should be inherited by an insertion at the given
|
|
14
|
+
* character offset. Mirrors Quill / ProseMirror behaviour: the inserted text
|
|
15
|
+
* adopts the marks of the character immediately to its left.
|
|
16
|
+
*
|
|
17
|
+
* At offset 0 there is no left character, so we return empty marks.
|
|
18
|
+
*/
|
|
19
|
+
export declare function inheritedMarksAt(yText: Y.Text, position: number): MarkAttrs;
|
|
20
|
+
/**
|
|
21
|
+
* Get the marks that are common to *every* character in `[from, to)`. Used by
|
|
22
|
+
* `toggleMark` to decide whether to add or remove.
|
|
23
|
+
*/
|
|
24
|
+
export declare function marksInRange(yText: Y.Text, from: number, to: number): MarkAttrs;
|
|
25
|
+
/**
|
|
26
|
+
* Apply a `replace` edit captured from the native view to the Y.Text.
|
|
27
|
+
*
|
|
28
|
+
* The edit is wrapped in a single Y.Doc transaction tagged with
|
|
29
|
+
* {@link ORIGIN_LOCAL_VIEW} so the observer loop in {@link YTextInput} knows
|
|
30
|
+
* to skip the resulting render-down (the view already has the change locally).
|
|
31
|
+
*/
|
|
32
|
+
export declare function applyReplaceEdit(yText: Y.Text, edit: ReplaceEdit, schema: Schema, onSchemaViolation?: (info: {
|
|
33
|
+
mark: string;
|
|
34
|
+
attrs: MarkAttrs;
|
|
35
|
+
}) => void, pendingMarks?: MarkAttrs): void;
|
|
36
|
+
/**
|
|
37
|
+
* Wrap a programmatic mutation (anything driven from JS — toolbar mark
|
|
38
|
+
* toggles, scripted inserts, deletes) in a Y.Doc transaction.
|
|
39
|
+
*
|
|
40
|
+
* Critically, we do NOT tag this with {@link ORIGIN_LOCAL_VIEW}. That origin
|
|
41
|
+
* is reserved for {@link applyReplaceEdit} — the round-trip path for text
|
|
42
|
+
* the user just typed into the native view, where the view *already* shows
|
|
43
|
+
* the change and re-pushing runs back down would clobber the in-flight IME
|
|
44
|
+
* composition / caret. Programmatic mutations, by contrast, haven't been
|
|
45
|
+
* reflected in the native view yet, so they MUST be observed by `YTextInput`
|
|
46
|
+
* to trigger an attributed-string update. Leaving the origin unset (or, in
|
|
47
|
+
* future, using a distinct `ORIGIN_LOCAL_PROGRAMMATIC` sentinel) is what
|
|
48
|
+
* causes the observer's `origin === ORIGIN_LOCAL_VIEW` short-circuit to
|
|
49
|
+
* fall through and recompute / repaint.
|
|
50
|
+
*/
|
|
51
|
+
export declare function transactProgrammatic(yText: Y.Text, fn: () => void): void;
|
|
52
|
+
/**
|
|
53
|
+
* Carrier for a caret/range expressed as relative positions. Survives concurrent
|
|
54
|
+
* remote insertions/deletions: when the underlying Y.Text changes, resolving
|
|
55
|
+
* the relative positions against the new state gives us the new absolute offsets
|
|
56
|
+
* that correspond to "the same logical place" the user was at.
|
|
57
|
+
*/
|
|
58
|
+
export interface RelativeSelection {
|
|
59
|
+
from: Y.RelativePosition;
|
|
60
|
+
to: Y.RelativePosition;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Capture the current absolute selection as a {@link RelativeSelection}.
|
|
64
|
+
*
|
|
65
|
+
* Y.RelativePosition has an "assoc" flag controlling which side of a tie the
|
|
66
|
+
* position lives on. For a caret we use:
|
|
67
|
+
* - `assoc = -1` for the `from` (start) — sticks to the *left* of a future
|
|
68
|
+
* insertion at the same offset, which is the conventional caret behaviour
|
|
69
|
+
* for the start of a selection
|
|
70
|
+
* - `assoc = 1` for the `to` (end) — sticks to the *right*
|
|
71
|
+
*
|
|
72
|
+
* For a collapsed caret both still work — the caret stays where the user "is".
|
|
73
|
+
*/
|
|
74
|
+
export declare function captureRelativeSelection(yText: Y.Text, selection: SelectionRange): RelativeSelection;
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a {@link RelativeSelection} against the current Y.Text state.
|
|
77
|
+
*
|
|
78
|
+
* Returns `null` if either endpoint can no longer be resolved (e.g. the Y.Text
|
|
79
|
+
* was completely cleared remotely). Callers should fall back to a sensible
|
|
80
|
+
* default — typically clamping the caret to `yText.length`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function resolveRelativeSelection(yText: Y.Text, sel: RelativeSelection): SelectionRange | null;
|
|
83
|
+
/**
|
|
84
|
+
* Apply (or remove) a mark across a range. Removal is requested by passing
|
|
85
|
+
* `attrs = null` (matches the Yjs `format()` semantics).
|
|
86
|
+
*/
|
|
87
|
+
export declare function formatRange(yText: Y.Text, from: number, to: number, markName: string, attrs: MarkAttrs | null): void;
|
|
88
|
+
//# sourceMappingURL=bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAGzB,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAGnF;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,GAAG,GAAG,EAAE,CAUhD;AAWD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,CAa3E;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,SAAS,CAyB/E;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,CAAC,CAAC,IAAI,EACb,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,MAAM,EACd,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,KAAK,IAAI,EACtE,YAAY,CAAC,EAAE,SAAS,GACvB,IAAI,CA2BN;AA4DD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAOxE;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,CAAC,CAAC,gBAAgB,CAAC;IACzB,EAAE,EAAE,CAAC,CAAC,gBAAgB,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,CAAC,CAAC,IAAI,EACb,SAAS,EAAE,cAAc,GACxB,iBAAiB,CAKnB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,CAAC,CAAC,IAAI,EACb,GAAG,EAAE,iBAAiB,GACrB,cAAc,GAAG,IAAI,CAOvB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,CAAC,CAAC,IAAI,EACb,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,SAAS,GAAG,IAAI,GACtB,IAAI,CAQN"}
|
package/build/bridge.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
import { validateMarks } from './schema';
|
|
3
|
+
import { ORIGIN_LOCAL_VIEW } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Convert a Y.Text into the array of attributed runs sent down to the native
|
|
6
|
+
* view as a prop.
|
|
7
|
+
*
|
|
8
|
+
* v0.1 only deals with string inserts — embeds (the future inline-non-text node
|
|
9
|
+
* story documented in the spec) are deferred. If the underlying delta contains
|
|
10
|
+
* a non-string insert we stringify defensively rather than throwing.
|
|
11
|
+
*/
|
|
12
|
+
export function ytextToRuns(yText) {
|
|
13
|
+
const delta = yText.toDelta();
|
|
14
|
+
const runs = [];
|
|
15
|
+
for (const op of delta) {
|
|
16
|
+
if (op.insert == null)
|
|
17
|
+
continue;
|
|
18
|
+
const text = typeof op.insert === 'string' ? op.insert : String(op.insert);
|
|
19
|
+
if (text.length === 0)
|
|
20
|
+
continue;
|
|
21
|
+
runs.push({ text, marks: op.attributes ?? {} });
|
|
22
|
+
}
|
|
23
|
+
return runs;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Compute the marks that should be inherited by an insertion at the given
|
|
27
|
+
* character offset. Mirrors Quill / ProseMirror behaviour: the inserted text
|
|
28
|
+
* adopts the marks of the character immediately to its left.
|
|
29
|
+
*
|
|
30
|
+
* At offset 0 there is no left character, so we return empty marks.
|
|
31
|
+
*/
|
|
32
|
+
export function inheritedMarksAt(yText, position) {
|
|
33
|
+
if (position <= 0)
|
|
34
|
+
return {};
|
|
35
|
+
const delta = yText.toDelta();
|
|
36
|
+
let cursor = 0;
|
|
37
|
+
for (const op of delta) {
|
|
38
|
+
const len = typeof op.insert === 'string' ? op.insert.length : op.insert ? 1 : 0;
|
|
39
|
+
if (len === 0)
|
|
40
|
+
continue;
|
|
41
|
+
if (cursor + len >= position) {
|
|
42
|
+
return op.attributes ? { ...op.attributes } : {};
|
|
43
|
+
}
|
|
44
|
+
cursor += len;
|
|
45
|
+
}
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get the marks that are common to *every* character in `[from, to)`. Used by
|
|
50
|
+
* `toggleMark` to decide whether to add or remove.
|
|
51
|
+
*/
|
|
52
|
+
export function marksInRange(yText, from, to) {
|
|
53
|
+
if (to <= from)
|
|
54
|
+
return inheritedMarksAt(yText, from);
|
|
55
|
+
const delta = yText.toDelta();
|
|
56
|
+
let cursor = 0;
|
|
57
|
+
let common = null;
|
|
58
|
+
for (const op of delta) {
|
|
59
|
+
const len = typeof op.insert === 'string' ? op.insert.length : op.insert ? 1 : 0;
|
|
60
|
+
if (len === 0)
|
|
61
|
+
continue;
|
|
62
|
+
const opStart = cursor;
|
|
63
|
+
const opEnd = cursor + len;
|
|
64
|
+
cursor = opEnd;
|
|
65
|
+
if (opEnd <= from)
|
|
66
|
+
continue;
|
|
67
|
+
if (opStart >= to)
|
|
68
|
+
break;
|
|
69
|
+
const attrs = op.attributes ?? {};
|
|
70
|
+
if (common === null) {
|
|
71
|
+
common = { ...attrs };
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
for (const key of Object.keys(common)) {
|
|
75
|
+
const a = JSON.stringify(common[key]);
|
|
76
|
+
const b = JSON.stringify(attrs[key]);
|
|
77
|
+
if (a !== b)
|
|
78
|
+
delete common[key];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return common ?? {};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Apply a `replace` edit captured from the native view to the Y.Text.
|
|
86
|
+
*
|
|
87
|
+
* The edit is wrapped in a single Y.Doc transaction tagged with
|
|
88
|
+
* {@link ORIGIN_LOCAL_VIEW} so the observer loop in {@link YTextInput} knows
|
|
89
|
+
* to skip the resulting render-down (the view already has the change locally).
|
|
90
|
+
*/
|
|
91
|
+
export function applyReplaceEdit(yText, edit, schema, onSchemaViolation, pendingMarks) {
|
|
92
|
+
const doc = yText.doc;
|
|
93
|
+
if (!doc) {
|
|
94
|
+
// Y.Text not attached to a doc — extremely defensive, shouldn't happen in
|
|
95
|
+
// normal use, but we'd rather no-op than crash.
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
doc.transact(() => {
|
|
99
|
+
const deleteLen = edit.to - edit.from;
|
|
100
|
+
if (deleteLen > 0) {
|
|
101
|
+
yText.delete(edit.from, deleteLen);
|
|
102
|
+
}
|
|
103
|
+
if (edit.text.length > 0) {
|
|
104
|
+
const insertAttrs = computeInsertAttrs(yText, edit.from, schema, pendingMarks, onSchemaViolation);
|
|
105
|
+
yText.insert(edit.from, edit.text, Object.keys(insertAttrs).length > 0 ? insertAttrs : undefined);
|
|
106
|
+
}
|
|
107
|
+
}, ORIGIN_LOCAL_VIEW);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Compute the attributes to pass to `Y.Text.insert(...)` for a new run of text
|
|
111
|
+
* at `position`, given the inherited marks of the character to the left and
|
|
112
|
+
* the editor's pending-marks overlay.
|
|
113
|
+
*
|
|
114
|
+
* Crucially, the returned object actively **`null`s out** any mark that's
|
|
115
|
+
* being suppressed — either because the user toggled it off via the pending
|
|
116
|
+
* overlay, or because the schema sanitiser dropped it (unknown mark, failed
|
|
117
|
+
* validation). Y.Text inheriting an attribute on the new characters can only
|
|
118
|
+
* be cancelled by passing `null` for that key; passing `undefined` or
|
|
119
|
+
* omitting the key keeps the inheritance.
|
|
120
|
+
*/
|
|
121
|
+
function computeInsertAttrs(yText, position, schema, pendingMarks, onSchemaViolation) {
|
|
122
|
+
const inherited = inheritedMarksAt(yText, position);
|
|
123
|
+
const pending = pendingMarks ?? {};
|
|
124
|
+
// Build the "intended" set of marks. Start from inherited, then apply
|
|
125
|
+
// pending: a truthy value adds/overrides, `false` removes.
|
|
126
|
+
const additions = { ...inherited };
|
|
127
|
+
const explicitRemovals = new Set();
|
|
128
|
+
for (const [name, value] of Object.entries(pending)) {
|
|
129
|
+
if (value === false) {
|
|
130
|
+
explicitRemovals.add(name);
|
|
131
|
+
delete additions[name];
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
additions[name] = value;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const { sanitised, violations } = validateMarks(additions, schema);
|
|
138
|
+
if (onSchemaViolation) {
|
|
139
|
+
for (const v of violations)
|
|
140
|
+
onSchemaViolation(v);
|
|
141
|
+
}
|
|
142
|
+
// Anything we *wanted* to keep but the sanitiser dropped (unknown mark,
|
|
143
|
+
// invalid attrs) should also be actively removed so it doesn't leak into
|
|
144
|
+
// the inserted characters via Y.Text inheritance.
|
|
145
|
+
for (const name of Object.keys(additions)) {
|
|
146
|
+
if (!(name in sanitised))
|
|
147
|
+
explicitRemovals.add(name);
|
|
148
|
+
}
|
|
149
|
+
// Inherited marks that the user didn't explicitly touch and that *did*
|
|
150
|
+
// survive sanitisation are already in `sanitised`. Inherited marks that
|
|
151
|
+
// *failed* sanitisation are caught by the loop above. We don't need to
|
|
152
|
+
// null any of them twice.
|
|
153
|
+
const out = { ...sanitised };
|
|
154
|
+
for (const name of explicitRemovals) {
|
|
155
|
+
out[name] = null;
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Wrap a programmatic mutation (anything driven from JS — toolbar mark
|
|
161
|
+
* toggles, scripted inserts, deletes) in a Y.Doc transaction.
|
|
162
|
+
*
|
|
163
|
+
* Critically, we do NOT tag this with {@link ORIGIN_LOCAL_VIEW}. That origin
|
|
164
|
+
* is reserved for {@link applyReplaceEdit} — the round-trip path for text
|
|
165
|
+
* the user just typed into the native view, where the view *already* shows
|
|
166
|
+
* the change and re-pushing runs back down would clobber the in-flight IME
|
|
167
|
+
* composition / caret. Programmatic mutations, by contrast, haven't been
|
|
168
|
+
* reflected in the native view yet, so they MUST be observed by `YTextInput`
|
|
169
|
+
* to trigger an attributed-string update. Leaving the origin unset (or, in
|
|
170
|
+
* future, using a distinct `ORIGIN_LOCAL_PROGRAMMATIC` sentinel) is what
|
|
171
|
+
* causes the observer's `origin === ORIGIN_LOCAL_VIEW` short-circuit to
|
|
172
|
+
* fall through and recompute / repaint.
|
|
173
|
+
*/
|
|
174
|
+
export function transactProgrammatic(yText, fn) {
|
|
175
|
+
const doc = yText.doc;
|
|
176
|
+
if (!doc) {
|
|
177
|
+
fn();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
doc.transact(fn);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Capture the current absolute selection as a {@link RelativeSelection}.
|
|
184
|
+
*
|
|
185
|
+
* Y.RelativePosition has an "assoc" flag controlling which side of a tie the
|
|
186
|
+
* position lives on. For a caret we use:
|
|
187
|
+
* - `assoc = -1` for the `from` (start) — sticks to the *left* of a future
|
|
188
|
+
* insertion at the same offset, which is the conventional caret behaviour
|
|
189
|
+
* for the start of a selection
|
|
190
|
+
* - `assoc = 1` for the `to` (end) — sticks to the *right*
|
|
191
|
+
*
|
|
192
|
+
* For a collapsed caret both still work — the caret stays where the user "is".
|
|
193
|
+
*/
|
|
194
|
+
export function captureRelativeSelection(yText, selection) {
|
|
195
|
+
return {
|
|
196
|
+
from: Y.createRelativePositionFromTypeIndex(yText, selection.from, -1),
|
|
197
|
+
to: Y.createRelativePositionFromTypeIndex(yText, selection.to, 1),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Resolve a {@link RelativeSelection} against the current Y.Text state.
|
|
202
|
+
*
|
|
203
|
+
* Returns `null` if either endpoint can no longer be resolved (e.g. the Y.Text
|
|
204
|
+
* was completely cleared remotely). Callers should fall back to a sensible
|
|
205
|
+
* default — typically clamping the caret to `yText.length`.
|
|
206
|
+
*/
|
|
207
|
+
export function resolveRelativeSelection(yText, sel) {
|
|
208
|
+
const doc = yText.doc;
|
|
209
|
+
if (!doc)
|
|
210
|
+
return null;
|
|
211
|
+
const from = Y.createAbsolutePositionFromRelativePosition(sel.from, doc);
|
|
212
|
+
const to = Y.createAbsolutePositionFromRelativePosition(sel.to, doc);
|
|
213
|
+
if (!from || !to || from.type !== yText || to.type !== yText)
|
|
214
|
+
return null;
|
|
215
|
+
return { from: from.index, to: to.index };
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Apply (or remove) a mark across a range. Removal is requested by passing
|
|
219
|
+
* `attrs = null` (matches the Yjs `format()` semantics).
|
|
220
|
+
*/
|
|
221
|
+
export function formatRange(yText, from, to, markName, attrs) {
|
|
222
|
+
if (to <= from)
|
|
223
|
+
return;
|
|
224
|
+
transactProgrammatic(yText, () => {
|
|
225
|
+
// Y.Text.format expects an attributes object where `null` values remove
|
|
226
|
+
// the corresponding mark. We pass the single mark either as its attrs
|
|
227
|
+
// object (apply) or as `null` (remove).
|
|
228
|
+
yText.format(from, to - from, { [markName]: attrs });
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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","sourcesContent":["import * as Y from 'yjs';\n\nimport { validateMarks } from './schema';\nimport type { MarkAttrs, ReplaceEdit, Run, Schema, SelectionRange } from './types';\nimport { ORIGIN_LOCAL_VIEW } from './types';\n\n/**\n * Convert a Y.Text into the array of attributed runs sent down to the native\n * view as a prop.\n *\n * v0.1 only deals with string inserts — embeds (the future inline-non-text node\n * story documented in the spec) are deferred. If the underlying delta contains\n * a non-string insert we stringify defensively rather than throwing.\n */\nexport function ytextToRuns(yText: Y.Text): Run[] {\n const delta = yText.toDelta() as YDeltaOp[];\n const runs: Run[] = [];\n for (const op of delta) {\n if (op.insert == null) continue;\n const text = typeof op.insert === 'string' ? op.insert : String(op.insert);\n if (text.length === 0) continue;\n runs.push({ text, marks: op.attributes ?? {} });\n }\n return runs;\n}\n\n/**\n * The shape `Y.Text.toDelta()` returns. Yjs's published types are loose here;\n * we narrow to what we actually consume.\n */\ninterface YDeltaOp {\n insert?: string | object;\n attributes?: MarkAttrs;\n}\n\n/**\n * Compute the marks that should be inherited by an insertion at the given\n * character offset. Mirrors Quill / ProseMirror behaviour: the inserted text\n * adopts the marks of the character immediately to its left.\n *\n * At offset 0 there is no left character, so we return empty marks.\n */\nexport function inheritedMarksAt(yText: Y.Text, position: number): MarkAttrs {\n if (position <= 0) return {};\n const delta = yText.toDelta() as YDeltaOp[];\n let cursor = 0;\n for (const op of delta) {\n const len = typeof op.insert === 'string' ? op.insert.length : op.insert ? 1 : 0;\n if (len === 0) continue;\n if (cursor + len >= position) {\n return op.attributes ? { ...op.attributes } : {};\n }\n cursor += len;\n }\n return {};\n}\n\n/**\n * Get the marks that are common to *every* character in `[from, to)`. Used by\n * `toggleMark` to decide whether to add or remove.\n */\nexport function marksInRange(yText: Y.Text, from: number, to: number): MarkAttrs {\n if (to <= from) return inheritedMarksAt(yText, from);\n const delta = yText.toDelta() as YDeltaOp[];\n let cursor = 0;\n let common: MarkAttrs | null = null;\n for (const op of delta) {\n const len = typeof op.insert === 'string' ? op.insert.length : op.insert ? 1 : 0;\n if (len === 0) continue;\n const opStart = cursor;\n const opEnd = cursor + len;\n cursor = opEnd;\n if (opEnd <= from) continue;\n if (opStart >= to) break;\n const attrs = op.attributes ?? {};\n if (common === null) {\n common = { ...attrs };\n } else {\n for (const key of Object.keys(common)) {\n const a = JSON.stringify(common[key]);\n const b = JSON.stringify(attrs[key]);\n if (a !== b) delete common[key];\n }\n }\n }\n return common ?? {};\n}\n\n/**\n * Apply a `replace` edit captured from the native view to the Y.Text.\n *\n * The edit is wrapped in a single Y.Doc transaction tagged with\n * {@link ORIGIN_LOCAL_VIEW} so the observer loop in {@link YTextInput} knows\n * to skip the resulting render-down (the view already has the change locally).\n */\nexport function applyReplaceEdit(\n yText: Y.Text,\n edit: ReplaceEdit,\n schema: Schema,\n onSchemaViolation?: (info: { mark: string; attrs: MarkAttrs }) => void,\n pendingMarks?: MarkAttrs\n): void {\n const doc = yText.doc;\n if (!doc) {\n // Y.Text not attached to a doc — extremely defensive, shouldn't happen in\n // normal use, but we'd rather no-op than crash.\n return;\n }\n doc.transact(() => {\n const deleteLen = edit.to - edit.from;\n if (deleteLen > 0) {\n yText.delete(edit.from, deleteLen);\n }\n if (edit.text.length > 0) {\n const insertAttrs = computeInsertAttrs(\n yText,\n edit.from,\n schema,\n pendingMarks,\n onSchemaViolation\n );\n yText.insert(\n edit.from,\n edit.text,\n Object.keys(insertAttrs).length > 0 ? insertAttrs : undefined\n );\n }\n }, ORIGIN_LOCAL_VIEW);\n}\n\n/**\n * Compute the attributes to pass to `Y.Text.insert(...)` for a new run of text\n * at `position`, given the inherited marks of the character to the left and\n * the editor's pending-marks overlay.\n *\n * Crucially, the returned object actively **`null`s out** any mark that's\n * being suppressed — either because the user toggled it off via the pending\n * overlay, or because the schema sanitiser dropped it (unknown mark, failed\n * validation). Y.Text inheriting an attribute on the new characters can only\n * be cancelled by passing `null` for that key; passing `undefined` or\n * omitting the key keeps the inheritance.\n */\nfunction computeInsertAttrs(\n yText: Y.Text,\n position: number,\n schema: Schema,\n pendingMarks: MarkAttrs | undefined,\n onSchemaViolation?: (info: { mark: string; attrs: MarkAttrs }) => void\n): MarkAttrs {\n const inherited = inheritedMarksAt(yText, position);\n const pending = pendingMarks ?? {};\n\n // Build the \"intended\" set of marks. Start from inherited, then apply\n // pending: a truthy value adds/overrides, `false` removes.\n const additions: MarkAttrs = { ...inherited };\n const explicitRemovals = new Set<string>();\n for (const [name, value] of Object.entries(pending)) {\n if (value === false) {\n explicitRemovals.add(name);\n delete additions[name];\n } else {\n additions[name] = value as MarkAttrs;\n }\n }\n\n const { sanitised, violations } = validateMarks(additions, schema);\n if (onSchemaViolation) {\n for (const v of violations) onSchemaViolation(v);\n }\n\n // Anything we *wanted* to keep but the sanitiser dropped (unknown mark,\n // invalid attrs) should also be actively removed so it doesn't leak into\n // the inserted characters via Y.Text inheritance.\n for (const name of Object.keys(additions)) {\n if (!(name in sanitised)) explicitRemovals.add(name);\n }\n // Inherited marks that the user didn't explicitly touch and that *did*\n // survive sanitisation are already in `sanitised`. Inherited marks that\n // *failed* sanitisation are caught by the loop above. We don't need to\n // null any of them twice.\n\n const out: MarkAttrs = { ...sanitised };\n for (const name of explicitRemovals) {\n out[name] = null;\n }\n return out;\n}\n\n/**\n * Wrap a programmatic mutation (anything driven from JS — toolbar mark\n * toggles, scripted inserts, deletes) in a Y.Doc transaction.\n *\n * Critically, we do NOT tag this with {@link ORIGIN_LOCAL_VIEW}. That origin\n * is reserved for {@link applyReplaceEdit} — the round-trip path for text\n * the user just typed into the native view, where the view *already* shows\n * the change and re-pushing runs back down would clobber the in-flight IME\n * composition / caret. Programmatic mutations, by contrast, haven't been\n * reflected in the native view yet, so they MUST be observed by `YTextInput`\n * to trigger an attributed-string update. Leaving the origin unset (or, in\n * future, using a distinct `ORIGIN_LOCAL_PROGRAMMATIC` sentinel) is what\n * causes the observer's `origin === ORIGIN_LOCAL_VIEW` short-circuit to\n * fall through and recompute / repaint.\n */\nexport function transactProgrammatic(yText: Y.Text, fn: () => void): void {\n const doc = yText.doc;\n if (!doc) {\n fn();\n return;\n }\n doc.transact(fn);\n}\n\n/**\n * Carrier for a caret/range expressed as relative positions. Survives concurrent\n * remote insertions/deletions: when the underlying Y.Text changes, resolving\n * the relative positions against the new state gives us the new absolute offsets\n * that correspond to \"the same logical place\" the user was at.\n */\nexport interface RelativeSelection {\n from: Y.RelativePosition;\n to: Y.RelativePosition;\n}\n\n/**\n * Capture the current absolute selection as a {@link RelativeSelection}.\n *\n * Y.RelativePosition has an \"assoc\" flag controlling which side of a tie the\n * position lives on. For a caret we use:\n * - `assoc = -1` for the `from` (start) — sticks to the *left* of a future\n * insertion at the same offset, which is the conventional caret behaviour\n * for the start of a selection\n * - `assoc = 1` for the `to` (end) — sticks to the *right*\n *\n * For a collapsed caret both still work — the caret stays where the user \"is\".\n */\nexport function captureRelativeSelection(\n yText: Y.Text,\n selection: SelectionRange\n): RelativeSelection {\n return {\n from: Y.createRelativePositionFromTypeIndex(yText, selection.from, -1),\n to: Y.createRelativePositionFromTypeIndex(yText, selection.to, 1),\n };\n}\n\n/**\n * Resolve a {@link RelativeSelection} against the current Y.Text state.\n *\n * Returns `null` if either endpoint can no longer be resolved (e.g. the Y.Text\n * was completely cleared remotely). Callers should fall back to a sensible\n * default — typically clamping the caret to `yText.length`.\n */\nexport function resolveRelativeSelection(\n yText: Y.Text,\n sel: RelativeSelection\n): SelectionRange | null {\n const doc = yText.doc;\n if (!doc) return null;\n const from = Y.createAbsolutePositionFromRelativePosition(sel.from, doc);\n const to = Y.createAbsolutePositionFromRelativePosition(sel.to, doc);\n if (!from || !to || from.type !== yText || to.type !== yText) return null;\n return { from: from.index, to: to.index };\n}\n\n/**\n * Apply (or remove) a mark across a range. Removal is requested by passing\n * `attrs = null` (matches the Yjs `format()` semantics).\n */\nexport function formatRange(\n yText: Y.Text,\n from: number,\n to: number,\n markName: string,\n attrs: MarkAttrs | null\n): void {\n if (to <= from) return;\n transactProgrammatic(yText, () => {\n // Y.Text.format expects an attributes object where `null` values remove\n // the corresponding mark. We pass the single mark either as its attrs\n // object (apply) or as `null` (remove).\n yText.format(from, to - from, { [markName]: attrs });\n });\n}\n"]}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @eclosion-tech/react-native-yjs-text
|
|
3
|
+
*
|
|
4
|
+
* Native React Native rich text editor backed by Y.Text. No WebView, no
|
|
5
|
+
* `contenteditable`, no DOM. See ./SPEC.md for the full design rationale.
|
|
6
|
+
*/
|
|
7
|
+
export { YTextInput } from './YTextInput';
|
|
8
|
+
export { YTextRenderer } from './YTextRenderer';
|
|
9
|
+
export { useYTextEditor } from './useYTextEditor';
|
|
10
|
+
export { defaultSchema } from './schema';
|
|
11
|
+
export type { MarkAttrs, MarkSpec, Run, Schema, SelectionRange, YTextEditor, YTextInputProps, YTextRendererProps, } from './types';
|
|
12
|
+
export { ORIGIN_LOCAL_VIEW } from './types';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,YAAY,EACV,SAAS,EACT,QAAQ,EACR,GAAG,EACH,MAAM,EACN,cAAc,EACd,WAAW,EACX,eAAe,EACf,kBAAkB,GACnB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @eclosion-tech/react-native-yjs-text
|
|
3
|
+
*
|
|
4
|
+
* Native React Native rich text editor backed by Y.Text. No WebView, no
|
|
5
|
+
* `contenteditable`, no DOM. See ./SPEC.md for the full design rationale.
|
|
6
|
+
*/
|
|
7
|
+
export { YTextInput } from './YTextInput';
|
|
8
|
+
export { YTextRenderer } from './YTextRenderer';
|
|
9
|
+
export { useYTextEditor } from './useYTextEditor';
|
|
10
|
+
export { defaultSchema } from './schema';
|
|
11
|
+
export { ORIGIN_LOCAL_VIEW } from './types';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAWzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC","sourcesContent":["/**\n * @eclosion-tech/react-native-yjs-text\n *\n * Native React Native rich text editor backed by Y.Text. No WebView, no\n * `contenteditable`, no DOM. See ./SPEC.md for the full design rationale.\n */\nexport { YTextInput } from './YTextInput';\nexport { YTextRenderer } from './YTextRenderer';\nexport { useYTextEditor } from './useYTextEditor';\nexport { defaultSchema } from './schema';\nexport type {\n MarkAttrs,\n MarkSpec,\n Run,\n Schema,\n SelectionRange,\n YTextEditor,\n YTextInputProps,\n YTextRendererProps,\n} from './types';\nexport { ORIGIN_LOCAL_VIEW } from './types';\n"]}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import type { CompiledRenderSpec } from '../schema';
|
|
4
|
+
import type { MarkAttrs, Run, SelectionRange } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* Selection bundle pushed to native as a single atomic prop.
|
|
7
|
+
*
|
|
8
|
+
* Bundling `from`/`to`/`version` into one Record (rather than two separate
|
|
9
|
+
* props on the native side) sidesteps Expo Modules' unspecified prop-setter
|
|
10
|
+
* ordering on Fabric: if the version prop were applied before the value,
|
|
11
|
+
* the native side would re-apply a stale selection and the user would see
|
|
12
|
+
* the previous selection "jump back" after every format toggle.
|
|
13
|
+
*/
|
|
14
|
+
export interface SerializedPendingSelection {
|
|
15
|
+
from: number;
|
|
16
|
+
to: number;
|
|
17
|
+
version: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A single run as it goes over the bridge to native. Marks are JSON-serialised
|
|
21
|
+
* because Expo Module `Record` fields don't cleanly accept heterogeneous
|
|
22
|
+
* `[String: Any]` dictionaries (mark attrs are arbitrary per the schema), and
|
|
23
|
+
* the cost of a JSON round-trip per run on every render is negligible compared
|
|
24
|
+
* to the layout work native then does.
|
|
25
|
+
*/
|
|
26
|
+
export interface SerializedRun {
|
|
27
|
+
text: string;
|
|
28
|
+
/** JSON-stringified `Record<string, unknown>` of mark name -> attrs. */
|
|
29
|
+
marksJson: string;
|
|
30
|
+
}
|
|
31
|
+
/** Serialise a public {@link Run} into the bridge representation. */
|
|
32
|
+
export declare function serializeRun(run: Run): SerializedRun;
|
|
33
|
+
/**
|
|
34
|
+
* Props sent to the native `YjsText` view across the bridge. Keep this shape
|
|
35
|
+
* stable — the iOS and Android sides parse it; adding a key requires native
|
|
36
|
+
* changes on both sides.
|
|
37
|
+
*/
|
|
38
|
+
export interface NativeYTextInputViewProps {
|
|
39
|
+
/**
|
|
40
|
+
* Attributed runs to render. Native always rebuilds when this prop
|
|
41
|
+
* changes — we used to ship a sibling `contentVersion: number` prop to
|
|
42
|
+
* force re-apply on attr-only changes, but Expo Modules' Fabric setter
|
|
43
|
+
* ordering didn't guarantee `runs` was applied before `contentVersion`,
|
|
44
|
+
* which caused every toggle to render the *previous* runs. Collapsing
|
|
45
|
+
* to a single trigger sidesteps the race entirely. The JS side prevents
|
|
46
|
+
* redundant pushes by memoising this array on a `contentVersion` state
|
|
47
|
+
* that bumps only on real Y.Text changes.
|
|
48
|
+
*/
|
|
49
|
+
runs: SerializedRun[];
|
|
50
|
+
/**
|
|
51
|
+
* Per-mark style spec. Inner keys are bounded by `RENDERABLE_TEXT_STYLE_KEYS`
|
|
52
|
+
* so this serialises cleanly into a typed Swift / Kotlin Record.
|
|
53
|
+
*/
|
|
54
|
+
renderSpec: CompiledRenderSpec;
|
|
55
|
+
/**
|
|
56
|
+
* Atomic selection bundle. Native re-applies whenever the embedded
|
|
57
|
+
* `version` differs from the last value it observed. `null` means "no
|
|
58
|
+
* JS-driven selection yet" — native leaves the in-flight user selection
|
|
59
|
+
* alone.
|
|
60
|
+
*/
|
|
61
|
+
pendingSelection: SerializedPendingSelection | null;
|
|
62
|
+
editable: boolean;
|
|
63
|
+
placeholder?: string;
|
|
64
|
+
placeholderColor?: string;
|
|
65
|
+
baseFontSize?: number;
|
|
66
|
+
baseFontFamily?: string;
|
|
67
|
+
baseColor?: string;
|
|
68
|
+
baseFontWeight?: string;
|
|
69
|
+
baseFontStyle?: string;
|
|
70
|
+
style?: StyleProp<ViewStyle>;
|
|
71
|
+
onContentChange?: (event: {
|
|
72
|
+
nativeEvent: {
|
|
73
|
+
type: 'replace';
|
|
74
|
+
from: number;
|
|
75
|
+
to: number;
|
|
76
|
+
text: string;
|
|
77
|
+
};
|
|
78
|
+
}) => void;
|
|
79
|
+
onNativeSelectionChange?: (event: {
|
|
80
|
+
nativeEvent: {
|
|
81
|
+
from: number;
|
|
82
|
+
to: number;
|
|
83
|
+
};
|
|
84
|
+
}) => void;
|
|
85
|
+
onFocusChange?: (event: {
|
|
86
|
+
nativeEvent: {
|
|
87
|
+
focused: boolean;
|
|
88
|
+
};
|
|
89
|
+
}) => void;
|
|
90
|
+
onMarkTap?: (event: {
|
|
91
|
+
nativeEvent: {
|
|
92
|
+
mark: string;
|
|
93
|
+
attrsJson: string;
|
|
94
|
+
};
|
|
95
|
+
}) => void;
|
|
96
|
+
}
|
|
97
|
+
/** Parse a mark-tap event payload from native back into a `MarkAttrs` object. */
|
|
98
|
+
export declare function parseMarkTapAttrs(attrsJson: string): MarkAttrs;
|
|
99
|
+
/**
|
|
100
|
+
* Direct handle to the underlying native `UITextView` / `AppCompatEditText`.
|
|
101
|
+
* Exposed via {@link React.useImperativeHandle} so the JS layer can call
|
|
102
|
+
* focus/blur/setSelection without going through props.
|
|
103
|
+
*
|
|
104
|
+
* Implemented as Expo Modules `AsyncFunction`s on the native side that take a
|
|
105
|
+
* view tag and dispatch to the matching view instance.
|
|
106
|
+
*/
|
|
107
|
+
export interface NativeYTextInputViewRef {
|
|
108
|
+
focus(): void;
|
|
109
|
+
blur(): void;
|
|
110
|
+
setSelection(range: SelectionRange): void;
|
|
111
|
+
isFocused(): boolean;
|
|
112
|
+
}
|
|
113
|
+
export declare const NativeYTextInputView: React.ForwardRefExoticComponent<NativeYTextInputViewProps & React.RefAttributes<NativeYTextInputViewRef>>;
|
|
114
|
+
//# sourceMappingURL=NativeYTextInputView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeYTextInputView.d.ts","sourceRoot":"","sources":["../../src/internal/NativeYTextInputView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/D;;;;;;;;GAQG;AACH,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,aAAa,CAKpD;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;;;;;OASG;IACH,IAAI,EAAE,aAAa,EAAE,CAAC;IACtB;;;OAGG;IACH,UAAU,EAAE,kBAAkB,CAAC;IAC/B;;;;;OAKG;IACH,gBAAgB,EAAE,0BAA0B,GAAG,IAAI,CAAC;IACpD,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAE7B,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE;QACxB,WAAW,EAAE;YAAE,IAAI,EAAE,SAAS,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;KAC1E,KAAK,IAAI,CAAC;IACX,uBAAuB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,KAAK,IAAI,CAAC;IACzF,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE;YAAE,OAAO,EAAE,OAAO,CAAA;SAAE,CAAA;KAAE,KAAK,IAAI,CAAC;IACvE,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,KAAK,IAAI,CAAC;CACnF;AAED,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAQ9D;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,uBAAuB;IACtC,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,IAAI,CAAC;IACb,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;IAC1C,SAAS,IAAI,OAAO,CAAC;CACtB;AAMD,eAAO,MAAM,oBAAoB,2GAK/B,CAAC"}
|