@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/LICENSE +21 -0
  3. package/README.md +323 -0
  4. package/SPEC.md +346 -0
  5. package/android/build.gradle +26 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/tech/eclosion/yjstext/YjsTextModule.kt +77 -0
  8. package/android/src/main/java/tech/eclosion/yjstext/YjsTextSupport.kt +135 -0
  9. package/android/src/main/java/tech/eclosion/yjstext/YjsTextView.kt +424 -0
  10. package/build/YTextInput.d.ts +23 -0
  11. package/build/YTextInput.d.ts.map +1 -0
  12. package/build/YTextInput.js +178 -0
  13. package/build/YTextInput.js.map +1 -0
  14. package/build/YTextRenderer.d.ts +15 -0
  15. package/build/YTextRenderer.d.ts.map +1 -0
  16. package/build/YTextRenderer.js +85 -0
  17. package/build/YTextRenderer.js.map +1 -0
  18. package/build/bridge.d.ts +88 -0
  19. package/build/bridge.d.ts.map +1 -0
  20. package/build/bridge.js +231 -0
  21. package/build/bridge.js.map +1 -0
  22. package/build/index.d.ts +13 -0
  23. package/build/index.d.ts.map +1 -0
  24. package/build/index.js +12 -0
  25. package/build/index.js.map +1 -0
  26. package/build/internal/NativeYTextInputView.d.ts +114 -0
  27. package/build/internal/NativeYTextInputView.d.ts.map +1 -0
  28. package/build/internal/NativeYTextInputView.js +27 -0
  29. package/build/internal/NativeYTextInputView.js.map +1 -0
  30. package/build/internal/editorRegistry.d.ts +23 -0
  31. package/build/internal/editorRegistry.d.ts.map +1 -0
  32. package/build/internal/editorRegistry.js +26 -0
  33. package/build/internal/editorRegistry.js.map +1 -0
  34. package/build/schema.d.ts +51 -0
  35. package/build/schema.d.ts.map +1 -0
  36. package/build/schema.js +134 -0
  37. package/build/schema.js.map +1 -0
  38. package/build/types.d.ts +182 -0
  39. package/build/types.d.ts.map +1 -0
  40. package/build/types.js +11 -0
  41. package/build/types.js.map +1 -0
  42. package/build/useYTextEditor.d.ts +21 -0
  43. package/build/useYTextEditor.d.ts.map +1 -0
  44. package/build/useYTextEditor.js +166 -0
  45. package/build/useYTextEditor.js.map +1 -0
  46. package/expo-module.config.json +9 -0
  47. package/ios/YjsText.podspec +30 -0
  48. package/ios/YjsTextModule.swift +75 -0
  49. package/ios/YjsTextSupport.swift +135 -0
  50. package/ios/YjsTextView.swift +464 -0
  51. package/package.json +124 -0
  52. package/src/YTextInput.tsx +263 -0
  53. package/src/YTextRenderer.tsx +96 -0
  54. package/src/bridge.ts +283 -0
  55. package/src/index.ts +21 -0
  56. package/src/internal/NativeYTextInputView.tsx +126 -0
  57. package/src/internal/editorRegistry.ts +50 -0
  58. package/src/schema.ts +157 -0
  59. package/src/types.ts +194 -0
  60. 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"}
@@ -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"]}
@@ -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"}