@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,27 @@
|
|
|
1
|
+
import { requireNativeView } from 'expo';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
/** Serialise a public {@link Run} into the bridge representation. */
|
|
4
|
+
export function serializeRun(run) {
|
|
5
|
+
return {
|
|
6
|
+
text: run.text,
|
|
7
|
+
marksJson: JSON.stringify(run.marks ?? {}),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** Parse a mark-tap event payload from native back into a `MarkAttrs` object. */
|
|
11
|
+
export function parseMarkTapAttrs(attrsJson) {
|
|
12
|
+
if (!attrsJson)
|
|
13
|
+
return {};
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(attrsJson);
|
|
16
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Resolved at module load: the Expo native view registered as `YjsText`. */
|
|
23
|
+
const NativeView = requireNativeView('YjsText');
|
|
24
|
+
export const NativeYTextInputView = React.forwardRef(function NativeYTextInputView(props, ref) {
|
|
25
|
+
return <NativeView {...props} ref={ref}/>;
|
|
26
|
+
});
|
|
27
|
+
//# sourceMappingURL=NativeYTextInputView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeYTextInputView.js","sourceRoot":"","sources":["../../src/internal/NativeYTextInputView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACzC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAkC/B,qEAAqE;AACrE,MAAM,UAAU,YAAY,CAAC,GAAQ;IACnC,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;KAC3C,CAAC;AACJ,CAAC;AAiDD,iFAAiF;AACjF,MAAM,UAAU,iBAAiB,CAAC,SAAiB;IACjD,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACrC,OAAO,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAiBD,6EAA6E;AAC7E,MAAM,UAAU,GACd,iBAAiB,CAAC,SAAS,CAAC,CAAC;AAE/B,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,CAAC,UAAU,CAGlD,SAAS,oBAAoB,CAAC,KAAK,EAAE,GAAG;IACxC,OAAO,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAG,CAAC;AAC7C,CAAC,CAAC,CAAC","sourcesContent":["import { requireNativeView } from 'expo';\nimport * as React from 'react';\nimport type { StyleProp, ViewStyle } from 'react-native';\n\nimport type { CompiledRenderSpec } from '../schema';\nimport type { MarkAttrs, Run, SelectionRange } from '../types';\n\n/**\n * Selection bundle pushed to native as a single atomic prop.\n *\n * Bundling `from`/`to`/`version` into one Record (rather than two separate\n * props on the native side) sidesteps Expo Modules' unspecified prop-setter\n * ordering on Fabric: if the version prop were applied before the value,\n * the native side would re-apply a stale selection and the user would see\n * the previous selection \"jump back\" after every format toggle.\n */\nexport interface SerializedPendingSelection {\n from: number;\n to: number;\n version: number;\n}\n\n/**\n * A single run as it goes over the bridge to native. Marks are JSON-serialised\n * because Expo Module `Record` fields don't cleanly accept heterogeneous\n * `[String: Any]` dictionaries (mark attrs are arbitrary per the schema), and\n * the cost of a JSON round-trip per run on every render is negligible compared\n * to the layout work native then does.\n */\nexport interface SerializedRun {\n text: string;\n /** JSON-stringified `Record<string, unknown>` of mark name -> attrs. */\n marksJson: string;\n}\n\n/** Serialise a public {@link Run} into the bridge representation. */\nexport function serializeRun(run: Run): SerializedRun {\n return {\n text: run.text,\n marksJson: JSON.stringify(run.marks ?? {}),\n };\n}\n\n/**\n * Props sent to the native `YjsText` view across the bridge. Keep this shape\n * stable — the iOS and Android sides parse it; adding a key requires native\n * changes on both sides.\n */\nexport interface NativeYTextInputViewProps {\n /**\n * Attributed runs to render. Native always rebuilds when this prop\n * changes — we used to ship a sibling `contentVersion: number` prop to\n * force re-apply on attr-only changes, but Expo Modules' Fabric setter\n * ordering didn't guarantee `runs` was applied before `contentVersion`,\n * which caused every toggle to render the *previous* runs. Collapsing\n * to a single trigger sidesteps the race entirely. The JS side prevents\n * redundant pushes by memoising this array on a `contentVersion` state\n * that bumps only on real Y.Text changes.\n */\n runs: SerializedRun[];\n /**\n * Per-mark style spec. Inner keys are bounded by `RENDERABLE_TEXT_STYLE_KEYS`\n * so this serialises cleanly into a typed Swift / Kotlin Record.\n */\n renderSpec: CompiledRenderSpec;\n /**\n * Atomic selection bundle. Native re-applies whenever the embedded\n * `version` differs from the last value it observed. `null` means \"no\n * JS-driven selection yet\" — native leaves the in-flight user selection\n * alone.\n */\n pendingSelection: SerializedPendingSelection | null;\n editable: boolean;\n placeholder?: string;\n placeholderColor?: string;\n baseFontSize?: number;\n baseFontFamily?: string;\n baseColor?: string;\n baseFontWeight?: string;\n baseFontStyle?: string;\n style?: StyleProp<ViewStyle>;\n\n onContentChange?: (event: {\n nativeEvent: { type: 'replace'; from: number; to: number; text: string };\n }) => void;\n onNativeSelectionChange?: (event: { nativeEvent: { from: number; to: number } }) => void;\n onFocusChange?: (event: { nativeEvent: { focused: boolean } }) => void;\n onMarkTap?: (event: { nativeEvent: { mark: string; attrsJson: string } }) => void;\n}\n\n/** Parse a mark-tap event payload from native back into a `MarkAttrs` object. */\nexport function parseMarkTapAttrs(attrsJson: string): MarkAttrs {\n if (!attrsJson) return {};\n try {\n const parsed = JSON.parse(attrsJson);\n return parsed && typeof parsed === 'object' ? parsed : {};\n } catch {\n return {};\n }\n}\n\n/**\n * Direct handle to the underlying native `UITextView` / `AppCompatEditText`.\n * Exposed via {@link React.useImperativeHandle} so the JS layer can call\n * focus/blur/setSelection without going through props.\n *\n * Implemented as Expo Modules `AsyncFunction`s on the native side that take a\n * view tag and dispatch to the matching view instance.\n */\nexport interface NativeYTextInputViewRef {\n focus(): void;\n blur(): void;\n setSelection(range: SelectionRange): void;\n isFocused(): boolean;\n}\n\n/** Resolved at module load: the Expo native view registered as `YjsText`. */\nconst NativeView: React.ComponentType<NativeYTextInputViewProps & { ref?: React.Ref<unknown> }> =\n requireNativeView('YjsText');\n\nexport const NativeYTextInputView = React.forwardRef<\n NativeYTextInputViewRef,\n NativeYTextInputViewProps\n>(function NativeYTextInputView(props, ref) {\n return <NativeView {...props} ref={ref} />;\n});\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type * as Y from 'yjs';
|
|
2
|
+
import type { MarkAttrs, SelectionRange } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* The slice of editor state a {@link YTextInput} contributes to the registry
|
|
5
|
+
* when it mounts. Lets the imperative {@link useYTextEditor} hook reach into
|
|
6
|
+
* the native view for focus / selection / etc. without prop-drilling refs.
|
|
7
|
+
*/
|
|
8
|
+
export interface EditorHandle {
|
|
9
|
+
/** Current absolute selection, or `null` if the view isn't focused. */
|
|
10
|
+
getSelection(): SelectionRange | null;
|
|
11
|
+
/** Pending marks set by `toggleMark` while the caret is collapsed. */
|
|
12
|
+
getPendingMarks(): MarkAttrs;
|
|
13
|
+
setPendingMarks(marks: MarkAttrs): void;
|
|
14
|
+
/** Imperative view operations forwarded to the native view. */
|
|
15
|
+
focus(): void;
|
|
16
|
+
blur(): void;
|
|
17
|
+
isFocused(): boolean;
|
|
18
|
+
setSelection(range: SelectionRange): void;
|
|
19
|
+
}
|
|
20
|
+
export declare function registerEditor(yText: Y.Text, handle: EditorHandle): void;
|
|
21
|
+
export declare function unregisterEditor(yText: Y.Text, handle: EditorHandle): void;
|
|
22
|
+
export declare function getEditor(yText: Y.Text): EditorHandle | undefined;
|
|
23
|
+
//# sourceMappingURL=editorRegistry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editorRegistry.d.ts","sourceRoot":"","sources":["../../src/internal/editorRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,CAAC,MAAM,KAAK,CAAC;AAE9B,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1D;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,YAAY,IAAI,cAAc,GAAG,IAAI,CAAC;IACtC,sEAAsE;IACtE,eAAe,IAAI,SAAS,CAAC;IAC7B,eAAe,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IACxC,+DAA+D;IAC/D,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,IAAI,CAAC;IACb,SAAS,IAAI,OAAO,CAAC;IACrB,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;CAC3C;AAcD,wBAAgB,cAAc,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAExE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAO1E;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,GAAG,YAAY,GAAG,SAAS,CAEjE"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry is keyed by Y.Text identity so that any consumer holding a
|
|
3
|
+
* reference to the same Y.Text can resolve the live editor handle without
|
|
4
|
+
* prop-drilling. A {@link Y.Text} maps to one handle at most — there's one
|
|
5
|
+
* active editor per logical text region by design (the per-block model
|
|
6
|
+
* documented in the spec).
|
|
7
|
+
*
|
|
8
|
+
* We use a `WeakMap` so unmounted editors don't pin their Y.Texts in memory
|
|
9
|
+
* forever.
|
|
10
|
+
*/
|
|
11
|
+
const REGISTRY = new WeakMap();
|
|
12
|
+
export function registerEditor(yText, handle) {
|
|
13
|
+
REGISTRY.set(yText, handle);
|
|
14
|
+
}
|
|
15
|
+
export function unregisterEditor(yText, handle) {
|
|
16
|
+
// Only unregister if the current entry is still us — protects against the
|
|
17
|
+
// race where a new editor for the same Y.Text mounts before the old one's
|
|
18
|
+
// cleanup runs.
|
|
19
|
+
if (REGISTRY.get(yText) === handle) {
|
|
20
|
+
REGISTRY.delete(yText);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function getEditor(yText) {
|
|
24
|
+
return REGISTRY.get(yText);
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=editorRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editorRegistry.js","sourceRoot":"","sources":["../../src/internal/editorRegistry.ts"],"names":[],"mappings":"AAsBA;;;;;;;;;GASG;AACH,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAwB,CAAC;AAErD,MAAM,UAAU,cAAc,CAAC,KAAa,EAAE,MAAoB;IAChE,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAa,EAAE,MAAoB;IAClE,0EAA0E;IAC1E,0EAA0E;IAC1E,gBAAgB;IAChB,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,MAAM,EAAE,CAAC;QACnC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC","sourcesContent":["import type * as Y from 'yjs';\n\nimport type { MarkAttrs, SelectionRange } from '../types';\n\n/**\n * The slice of editor state a {@link YTextInput} contributes to the registry\n * when it mounts. Lets the imperative {@link useYTextEditor} hook reach into\n * the native view for focus / selection / etc. without prop-drilling refs.\n */\nexport interface EditorHandle {\n /** Current absolute selection, or `null` if the view isn't focused. */\n getSelection(): SelectionRange | null;\n /** Pending marks set by `toggleMark` while the caret is collapsed. */\n getPendingMarks(): MarkAttrs;\n setPendingMarks(marks: MarkAttrs): void;\n /** Imperative view operations forwarded to the native view. */\n focus(): void;\n blur(): void;\n isFocused(): boolean;\n setSelection(range: SelectionRange): void;\n}\n\n/**\n * The registry is keyed by Y.Text identity so that any consumer holding a\n * reference to the same Y.Text can resolve the live editor handle without\n * prop-drilling. A {@link Y.Text} maps to one handle at most — there's one\n * active editor per logical text region by design (the per-block model\n * documented in the spec).\n *\n * We use a `WeakMap` so unmounted editors don't pin their Y.Texts in memory\n * forever.\n */\nconst REGISTRY = new WeakMap<Y.Text, EditorHandle>();\n\nexport function registerEditor(yText: Y.Text, handle: EditorHandle): void {\n REGISTRY.set(yText, handle);\n}\n\nexport function unregisterEditor(yText: Y.Text, handle: EditorHandle): void {\n // Only unregister if the current entry is still us — protects against the\n // race where a new editor for the same Y.Text mounts before the old one's\n // cleanup runs.\n if (REGISTRY.get(yText) === handle) {\n REGISTRY.delete(yText);\n }\n}\n\nexport function getEditor(yText: Y.Text): EditorHandle | undefined {\n return REGISTRY.get(yText);\n}\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { MarkAttrs, Schema } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* The default schema covering the v0.1 canonical mark set documented in the
|
|
4
|
+
* spec. Consumers can use this as-is, extend it, or replace it entirely.
|
|
5
|
+
*
|
|
6
|
+
* Mark names and semantics match the y-prosemirror schema convention so that a
|
|
7
|
+
* Y.Text edited on web via ProseMirror and on mobile via this library renders
|
|
8
|
+
* identically.
|
|
9
|
+
*/
|
|
10
|
+
export declare const defaultSchema: Schema;
|
|
11
|
+
/**
|
|
12
|
+
* Subset of `TextStyle` keys forwarded to the native side. Keys outside this
|
|
13
|
+
* list are silently ignored — they may make sense on the web but have no
|
|
14
|
+
* cross-platform native equivalent, or require per-platform work we haven't
|
|
15
|
+
* done yet.
|
|
16
|
+
*
|
|
17
|
+
* If you need a key not listed here, open an issue. Adding one usually means
|
|
18
|
+
* teaching both the iOS NSAttributedString and Android Spannable layers how to
|
|
19
|
+
* apply it.
|
|
20
|
+
*/
|
|
21
|
+
export declare const RENDERABLE_TEXT_STYLE_KEYS: readonly ["fontWeight", "fontStyle", "fontFamily", "fontSize", "color", "backgroundColor", "textDecorationLine"];
|
|
22
|
+
/**
|
|
23
|
+
* The compiled-down render spec sent over the bridge.
|
|
24
|
+
*
|
|
25
|
+
* One entry per mark, containing only the keys the native renderer knows how
|
|
26
|
+
* to handle. We compile once when the schema prop changes rather than parsing
|
|
27
|
+
* the whole `MarkSpec` on every render of every run.
|
|
28
|
+
*/
|
|
29
|
+
export type CompiledRenderSpec = Record<string, Partial<Record<(typeof RENDERABLE_TEXT_STYLE_KEYS)[number], unknown>>>;
|
|
30
|
+
/**
|
|
31
|
+
* Reduce a {@link Schema} to its renderable subset. Strips function callbacks
|
|
32
|
+
* (`onTap`), strips attribute schemas, normalises `textDecorationLine` to a
|
|
33
|
+
* single canonical string. Stable for memoisation: same input → equal output.
|
|
34
|
+
*/
|
|
35
|
+
export declare function compileRenderSpec(schema: Schema): CompiledRenderSpec;
|
|
36
|
+
/**
|
|
37
|
+
* Validate that every mark in `attrs` is declared in the schema and that its
|
|
38
|
+
* attributes match the declared shape. Returns the sanitised attrs (with
|
|
39
|
+
* undeclared marks stripped) and the list of violations.
|
|
40
|
+
*
|
|
41
|
+
* Used on insert paths (programmatic `insertText`, future paste handlers) to
|
|
42
|
+
* keep AI-generated or pasted content from smuggling in unknown marks.
|
|
43
|
+
*/
|
|
44
|
+
export declare function validateMarks(marks: MarkAttrs, schema: Schema): {
|
|
45
|
+
sanitised: MarkAttrs;
|
|
46
|
+
violations: {
|
|
47
|
+
mark: string;
|
|
48
|
+
attrs: MarkAttrs;
|
|
49
|
+
}[];
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEjD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,EAAE,MA8B3B,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,0BAA0B,kHAQU,CAAC;AAElD;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CACrC,MAAM,EACN,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,CACtE,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,CAkBpE;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,MAAM,GACb;IACD,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,EAAE,CAAC;CAClD,CAyCA"}
|
package/build/schema.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default schema covering the v0.1 canonical mark set documented in the
|
|
3
|
+
* spec. Consumers can use this as-is, extend it, or replace it entirely.
|
|
4
|
+
*
|
|
5
|
+
* Mark names and semantics match the y-prosemirror schema convention so that a
|
|
6
|
+
* Y.Text edited on web via ProseMirror and on mobile via this library renders
|
|
7
|
+
* identically.
|
|
8
|
+
*/
|
|
9
|
+
export const defaultSchema = {
|
|
10
|
+
marks: {
|
|
11
|
+
bold: {
|
|
12
|
+
renderStyle: { fontWeight: 'bold' },
|
|
13
|
+
},
|
|
14
|
+
italic: {
|
|
15
|
+
renderStyle: { fontStyle: 'italic' },
|
|
16
|
+
},
|
|
17
|
+
underline: {
|
|
18
|
+
renderStyle: { textDecorationLine: 'underline' },
|
|
19
|
+
},
|
|
20
|
+
strike: {
|
|
21
|
+
renderStyle: { textDecorationLine: 'line-through' },
|
|
22
|
+
},
|
|
23
|
+
code: {
|
|
24
|
+
renderStyle: {
|
|
25
|
+
fontFamily: 'Menlo',
|
|
26
|
+
backgroundColor: '#f4f4f4',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
link: {
|
|
30
|
+
attrs: {
|
|
31
|
+
href: { type: 'string', required: true },
|
|
32
|
+
},
|
|
33
|
+
renderStyle: {
|
|
34
|
+
color: '#0066cc',
|
|
35
|
+
textDecorationLine: 'underline',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Subset of `TextStyle` keys forwarded to the native side. Keys outside this
|
|
42
|
+
* list are silently ignored — they may make sense on the web but have no
|
|
43
|
+
* cross-platform native equivalent, or require per-platform work we haven't
|
|
44
|
+
* done yet.
|
|
45
|
+
*
|
|
46
|
+
* If you need a key not listed here, open an issue. Adding one usually means
|
|
47
|
+
* teaching both the iOS NSAttributedString and Android Spannable layers how to
|
|
48
|
+
* apply it.
|
|
49
|
+
*/
|
|
50
|
+
export const RENDERABLE_TEXT_STYLE_KEYS = [
|
|
51
|
+
'fontWeight',
|
|
52
|
+
'fontStyle',
|
|
53
|
+
'fontFamily',
|
|
54
|
+
'fontSize',
|
|
55
|
+
'color',
|
|
56
|
+
'backgroundColor',
|
|
57
|
+
'textDecorationLine',
|
|
58
|
+
];
|
|
59
|
+
/**
|
|
60
|
+
* Reduce a {@link Schema} to its renderable subset. Strips function callbacks
|
|
61
|
+
* (`onTap`), strips attribute schemas, normalises `textDecorationLine` to a
|
|
62
|
+
* single canonical string. Stable for memoisation: same input → equal output.
|
|
63
|
+
*/
|
|
64
|
+
export function compileRenderSpec(schema) {
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const [name, spec] of Object.entries(schema.marks)) {
|
|
67
|
+
const style = spec.renderStyle;
|
|
68
|
+
if (!style) {
|
|
69
|
+
out[name] = {};
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const compiled = {};
|
|
73
|
+
for (const key of RENDERABLE_TEXT_STYLE_KEYS) {
|
|
74
|
+
const value = style[key];
|
|
75
|
+
if (value !== undefined) {
|
|
76
|
+
compiled[key] = value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
out[name] = compiled;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Validate that every mark in `attrs` is declared in the schema and that its
|
|
85
|
+
* attributes match the declared shape. Returns the sanitised attrs (with
|
|
86
|
+
* undeclared marks stripped) and the list of violations.
|
|
87
|
+
*
|
|
88
|
+
* Used on insert paths (programmatic `insertText`, future paste handlers) to
|
|
89
|
+
* keep AI-generated or pasted content from smuggling in unknown marks.
|
|
90
|
+
*/
|
|
91
|
+
export function validateMarks(marks, schema) {
|
|
92
|
+
const sanitised = {};
|
|
93
|
+
const violations = [];
|
|
94
|
+
for (const [markName, attrs] of Object.entries(marks)) {
|
|
95
|
+
const spec = schema.marks[markName];
|
|
96
|
+
if (!spec) {
|
|
97
|
+
violations.push({ mark: markName, attrs: attrs ?? {} });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (!spec.attrs) {
|
|
101
|
+
sanitised[markName] = attrs ?? {};
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const incoming = (attrs ?? {});
|
|
105
|
+
const validated = {};
|
|
106
|
+
let ok = true;
|
|
107
|
+
for (const [attrName, attrSpec] of Object.entries(spec.attrs)) {
|
|
108
|
+
const value = incoming[attrName];
|
|
109
|
+
if (value === undefined) {
|
|
110
|
+
if (attrSpec.required && attrSpec.default === undefined) {
|
|
111
|
+
ok = false;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
if (attrSpec.default !== undefined) {
|
|
115
|
+
validated[attrName] = attrSpec.default;
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (typeof value !== attrSpec.type) {
|
|
120
|
+
ok = false;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
validated[attrName] = value;
|
|
124
|
+
}
|
|
125
|
+
if (ok) {
|
|
126
|
+
sanitised[markName] = validated;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
violations.push({ mark: markName, attrs: incoming });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { sanitised, violations };
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TextStyle } from 'react-native';\n\nimport type { MarkAttrs, Schema } from './types';\n\n/**\n * The default schema covering the v0.1 canonical mark set documented in the\n * spec. Consumers can use this as-is, extend it, or replace it entirely.\n *\n * Mark names and semantics match the y-prosemirror schema convention so that a\n * Y.Text edited on web via ProseMirror and on mobile via this library renders\n * identically.\n */\nexport const defaultSchema: Schema = {\n marks: {\n bold: {\n renderStyle: { fontWeight: 'bold' },\n },\n italic: {\n renderStyle: { fontStyle: 'italic' },\n },\n underline: {\n renderStyle: { textDecorationLine: 'underline' },\n },\n strike: {\n renderStyle: { textDecorationLine: 'line-through' },\n },\n code: {\n renderStyle: {\n fontFamily: 'Menlo',\n backgroundColor: '#f4f4f4',\n },\n },\n link: {\n attrs: {\n href: { type: 'string', required: true },\n },\n renderStyle: {\n color: '#0066cc',\n textDecorationLine: 'underline',\n },\n },\n },\n};\n\n/**\n * Subset of `TextStyle` keys forwarded to the native side. Keys outside this\n * list are silently ignored — they may make sense on the web but have no\n * cross-platform native equivalent, or require per-platform work we haven't\n * done yet.\n *\n * If you need a key not listed here, open an issue. Adding one usually means\n * teaching both the iOS NSAttributedString and Android Spannable layers how to\n * apply it.\n */\nexport const RENDERABLE_TEXT_STYLE_KEYS = [\n 'fontWeight',\n 'fontStyle',\n 'fontFamily',\n 'fontSize',\n 'color',\n 'backgroundColor',\n 'textDecorationLine',\n] as const satisfies readonly (keyof TextStyle)[];\n\n/**\n * The compiled-down render spec sent over the bridge.\n *\n * One entry per mark, containing only the keys the native renderer knows how\n * to handle. We compile once when the schema prop changes rather than parsing\n * the whole `MarkSpec` on every render of every run.\n */\nexport type CompiledRenderSpec = Record<\n string,\n Partial<Record<(typeof RENDERABLE_TEXT_STYLE_KEYS)[number], unknown>>\n>;\n\n/**\n * Reduce a {@link Schema} to its renderable subset. Strips function callbacks\n * (`onTap`), strips attribute schemas, normalises `textDecorationLine` to a\n * single canonical string. Stable for memoisation: same input → equal output.\n */\nexport function compileRenderSpec(schema: Schema): CompiledRenderSpec {\n const out: CompiledRenderSpec = {};\n for (const [name, spec] of Object.entries(schema.marks)) {\n const style = spec.renderStyle;\n if (!style) {\n out[name] = {};\n continue;\n }\n const compiled: CompiledRenderSpec[string] = {};\n for (const key of RENDERABLE_TEXT_STYLE_KEYS) {\n const value = style[key];\n if (value !== undefined) {\n compiled[key] = value;\n }\n }\n out[name] = compiled;\n }\n return out;\n}\n\n/**\n * Validate that every mark in `attrs` is declared in the schema and that its\n * attributes match the declared shape. Returns the sanitised attrs (with\n * undeclared marks stripped) and the list of violations.\n *\n * Used on insert paths (programmatic `insertText`, future paste handlers) to\n * keep AI-generated or pasted content from smuggling in unknown marks.\n */\nexport function validateMarks(\n marks: MarkAttrs,\n schema: Schema\n): {\n sanitised: MarkAttrs;\n violations: { mark: string; attrs: MarkAttrs }[];\n} {\n const sanitised: MarkAttrs = {};\n const violations: { mark: string; attrs: MarkAttrs }[] = [];\n for (const [markName, attrs] of Object.entries(marks)) {\n const spec = schema.marks[markName];\n if (!spec) {\n violations.push({ mark: markName, attrs: (attrs as MarkAttrs) ?? {} });\n continue;\n }\n if (!spec.attrs) {\n sanitised[markName] = attrs ?? {};\n continue;\n }\n const incoming = (attrs ?? {}) as MarkAttrs;\n const validated: MarkAttrs = {};\n let ok = true;\n for (const [attrName, attrSpec] of Object.entries(spec.attrs)) {\n const value = incoming[attrName];\n if (value === undefined) {\n if (attrSpec.required && attrSpec.default === undefined) {\n ok = false;\n break;\n }\n if (attrSpec.default !== undefined) {\n validated[attrName] = attrSpec.default;\n }\n continue;\n }\n if (typeof value !== attrSpec.type) {\n ok = false;\n break;\n }\n validated[attrName] = value;\n }\n if (ok) {\n sanitised[markName] = validated;\n } else {\n violations.push({ mark: markName, attrs: incoming });\n }\n }\n return { sanitised, violations };\n}\n"]}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { StyleProp, TextStyle } from 'react-native';
|
|
2
|
+
import type * as Y from 'yjs';
|
|
3
|
+
/**
|
|
4
|
+
* A formatting attribute attached to a span of text inside a {@link Y.Text}.
|
|
5
|
+
*
|
|
6
|
+
* Marks are the unit of inline formatting. They have a name (`bold`, `italic`,
|
|
7
|
+
* `link`, etc.) and an optional bag of attributes (`href` for links, `level` for
|
|
8
|
+
* headings if you ever invent one, etc.). They mirror the y-prosemirror mark
|
|
9
|
+
* convention so the same Y.Text content edits identically on web (via
|
|
10
|
+
* y-prosemirror over a ProseMirror schema) and on mobile (via this library).
|
|
11
|
+
*/
|
|
12
|
+
export type MarkAttrs = Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Declaration of a single mark in a {@link Schema}.
|
|
15
|
+
*
|
|
16
|
+
* @property attrs Optional schema for the mark's attributes. Validated on insert.
|
|
17
|
+
* If a mark takes no attributes (e.g. `bold`, `italic`), omit this entirely.
|
|
18
|
+
* @property renderStyle The style applied to spans bearing this mark when the
|
|
19
|
+
* editor renders to native text. Only a subset of RN's `TextStyle` is honoured
|
|
20
|
+
* on native (see {@link RENDERABLE_TEXT_STYLE_KEYS}); other keys are ignored.
|
|
21
|
+
* @property onTap Optional handler invoked when the user taps a span bearing
|
|
22
|
+
* this mark. The classic use case is `link` opening a URL. The handler runs in
|
|
23
|
+
* JS regardless of which platform fired the tap.
|
|
24
|
+
*/
|
|
25
|
+
export interface MarkSpec {
|
|
26
|
+
attrs?: Record<string, {
|
|
27
|
+
type: 'string' | 'number' | 'boolean';
|
|
28
|
+
required?: boolean;
|
|
29
|
+
default?: unknown;
|
|
30
|
+
}>;
|
|
31
|
+
renderStyle?: TextStyle;
|
|
32
|
+
onTap?: (attrs: MarkAttrs) => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The set of marks an editor instance accepts.
|
|
36
|
+
*
|
|
37
|
+
* The schema is the contract between the consuming application and the editor.
|
|
38
|
+
* Marks not declared here are dropped on insert (with an optional warning, see
|
|
39
|
+
* {@link YTextInputProps.onSchemaViolation}). This is what makes the editor safe
|
|
40
|
+
* for AI-generated content: an LLM cannot smuggle in `<script>`-equivalent
|
|
41
|
+
* marks that the consumer hasn't explicitly opted into.
|
|
42
|
+
*/
|
|
43
|
+
export interface Schema {
|
|
44
|
+
marks: Record<string, MarkSpec>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A contiguous run of text bearing the same formatting marks.
|
|
48
|
+
*
|
|
49
|
+
* This is the wire format used to push attributed text down to the native view.
|
|
50
|
+
* A {@link Y.Text} is converted to `Run[]` via {@link ytextToRuns}; the native
|
|
51
|
+
* side renders each run with the styles declared in its schema's marks.
|
|
52
|
+
*/
|
|
53
|
+
export interface Run {
|
|
54
|
+
text: string;
|
|
55
|
+
marks: MarkAttrs;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A character-offset range inside a {@link Y.Text}.
|
|
59
|
+
*
|
|
60
|
+
* Offsets are UTF-16 code units, matching `Y.Text`'s native offsets, RN's
|
|
61
|
+
* `TextInput` selection, iOS `NSRange`, and Android `Editable.getSelectionStart`.
|
|
62
|
+
* A collapsed cursor is represented by `from === to`.
|
|
63
|
+
*/
|
|
64
|
+
export interface SelectionRange {
|
|
65
|
+
from: number;
|
|
66
|
+
to: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The shape of an edit emitted by the native view when the user types, pastes,
|
|
70
|
+
* or deletes.
|
|
71
|
+
*
|
|
72
|
+
* v0.1 uses a single `replace`-shaped edit that subsumes insert (`from === to`),
|
|
73
|
+
* delete (`text === ''`), and replace. This matches iOS's
|
|
74
|
+
* `textView(_:shouldChangeTextIn:replacementText:)` directly.
|
|
75
|
+
*
|
|
76
|
+
* @internal
|
|
77
|
+
*/
|
|
78
|
+
export interface ReplaceEdit {
|
|
79
|
+
type: 'replace';
|
|
80
|
+
from: number;
|
|
81
|
+
to: number;
|
|
82
|
+
text: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Props for the {@link YTextInput} editable component.
|
|
86
|
+
*/
|
|
87
|
+
export interface YTextInputProps {
|
|
88
|
+
/** The Y.Text whose content this editor displays and mutates. */
|
|
89
|
+
yText: Y.Text;
|
|
90
|
+
/** The set of marks accepted by this editor. See {@link Schema}. */
|
|
91
|
+
schema: Schema;
|
|
92
|
+
/** RN text style applied as the baseline (font family, size, default color). */
|
|
93
|
+
style?: StyleProp<TextStyle>;
|
|
94
|
+
/** Placeholder text shown when the Y.Text is empty. */
|
|
95
|
+
placeholder?: string;
|
|
96
|
+
/** Colour for the placeholder text. Defaults to a muted grey. */
|
|
97
|
+
placeholderTextColor?: string;
|
|
98
|
+
/** Auto-focus on mount (raises the keyboard). Defaults to `false`. */
|
|
99
|
+
autoFocus?: boolean;
|
|
100
|
+
/** Whether the editor accepts input. Defaults to `true`. */
|
|
101
|
+
editable?: boolean;
|
|
102
|
+
/** Fires whenever the user moves the caret or changes the selected range. */
|
|
103
|
+
onSelectionChange?: (selection: SelectionRange) => void;
|
|
104
|
+
/** Fires when the editor gains focus. */
|
|
105
|
+
onFocus?: () => void;
|
|
106
|
+
/** Fires when the editor loses focus. */
|
|
107
|
+
onBlur?: () => void;
|
|
108
|
+
/**
|
|
109
|
+
* Fires when an insert contains a mark not declared in the schema.
|
|
110
|
+
* The mark is dropped silently regardless; this is your hook to log the
|
|
111
|
+
* violation (e.g. when an LLM generates an unrecognised mark).
|
|
112
|
+
*/
|
|
113
|
+
onSchemaViolation?: (info: {
|
|
114
|
+
mark: string;
|
|
115
|
+
attrs: MarkAttrs;
|
|
116
|
+
}) => void;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Props for the {@link YTextRenderer} read-only component.
|
|
120
|
+
*/
|
|
121
|
+
export interface YTextRendererProps {
|
|
122
|
+
/** The Y.Text to render. */
|
|
123
|
+
yText: Y.Text;
|
|
124
|
+
/** The schema describing how to render each mark. */
|
|
125
|
+
schema: Schema;
|
|
126
|
+
/** RN text style applied as the baseline. */
|
|
127
|
+
style?: StyleProp<TextStyle>;
|
|
128
|
+
/** Maximum lines to render before truncation. */
|
|
129
|
+
numberOfLines?: number;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The imperative editor API returned by {@link useYTextEditor}.
|
|
133
|
+
*
|
|
134
|
+
* All commands operate at the current selection unless given explicit ranges.
|
|
135
|
+
* Commands that mutate (`toggleMark`, `insertText`, ...) wrap the mutation in a
|
|
136
|
+
* Y.Doc transaction tagged with the {@link ORIGIN_LOCAL_VIEW} origin so the
|
|
137
|
+
* native view doesn't re-render itself in response.
|
|
138
|
+
*/
|
|
139
|
+
export interface YTextEditor {
|
|
140
|
+
/** Current selection, or `null` if the editor isn't focused. */
|
|
141
|
+
getSelection(): SelectionRange | null;
|
|
142
|
+
/** Move the cursor / select a range. No-op if the editor isn't mounted. */
|
|
143
|
+
setSelection(range: SelectionRange): void;
|
|
144
|
+
/**
|
|
145
|
+
* Toggle the mark across the current selection. If the entire selection
|
|
146
|
+
* already has the mark, removes it; otherwise applies it.
|
|
147
|
+
*/
|
|
148
|
+
toggleMark(name: string, attrs?: MarkAttrs): void;
|
|
149
|
+
/** Force-apply the mark across the current selection. */
|
|
150
|
+
setMark(name: string, attrs?: MarkAttrs): void;
|
|
151
|
+
/** Remove the mark from the current selection. */
|
|
152
|
+
removeMark(name: string): void;
|
|
153
|
+
/**
|
|
154
|
+
* Marks currently active at the selection's start.
|
|
155
|
+
*
|
|
156
|
+
* For a collapsed selection this is the marks of the character to the left,
|
|
157
|
+
* plus any `pending marks` (marks toggled while the selection was collapsed,
|
|
158
|
+
* which apply to the next character typed).
|
|
159
|
+
*/
|
|
160
|
+
marksAtSelection(): MarkAttrs;
|
|
161
|
+
/** Insert text at the current selection, replacing any selected range. */
|
|
162
|
+
insertText(text: string, attrs?: MarkAttrs): void;
|
|
163
|
+
/** Delete the given range. Offsets are in UTF-16 code units. */
|
|
164
|
+
deleteRange(from: number, to: number): void;
|
|
165
|
+
/** Raise the keyboard / programmatically focus the editor. */
|
|
166
|
+
focus(): void;
|
|
167
|
+
/** Dismiss the keyboard / blur the editor. */
|
|
168
|
+
blur(): void;
|
|
169
|
+
/** Whether the editor currently has keyboard focus. */
|
|
170
|
+
isFocused(): boolean;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The Yjs transaction origin used by this library when applying user edits
|
|
174
|
+
* captured from the native view, or programmatic edits from {@link YTextEditor}.
|
|
175
|
+
*
|
|
176
|
+
* Consumers can read `transaction.origin === ORIGIN_LOCAL_VIEW` to distinguish
|
|
177
|
+
* edits coming from the editor itself from edits coming from sync providers
|
|
178
|
+
* (`y-websocket`, your custom backend, etc.). The library uses it internally to
|
|
179
|
+
* avoid re-rendering the view in response to its own writes.
|
|
180
|
+
*/
|
|
181
|
+
export declare const ORIGIN_LOCAL_VIEW: unique symbol;
|
|
182
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Yjs transaction origin used by this library when applying user edits
|
|
3
|
+
* captured from the native view, or programmatic edits from {@link YTextEditor}.
|
|
4
|
+
*
|
|
5
|
+
* Consumers can read `transaction.origin === ORIGIN_LOCAL_VIEW` to distinguish
|
|
6
|
+
* edits coming from the editor itself from edits coming from sync providers
|
|
7
|
+
* (`y-websocket`, your custom backend, etc.). The library uses it internally to
|
|
8
|
+
* avoid re-rendering the view in response to its own writes.
|
|
9
|
+
*/
|
|
10
|
+
export const ORIGIN_LOCAL_VIEW = Symbol('@eclosion-tech/react-native-yjs-text/local-view');
|
|
11
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAwLA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC,iDAAiD,CAAC,CAAC","sourcesContent":["import type { StyleProp, TextStyle } from 'react-native';\nimport type * as Y from 'yjs';\n\n/**\n * A formatting attribute attached to a span of text inside a {@link Y.Text}.\n *\n * Marks are the unit of inline formatting. They have a name (`bold`, `italic`,\n * `link`, etc.) and an optional bag of attributes (`href` for links, `level` for\n * headings if you ever invent one, etc.). They mirror the y-prosemirror mark\n * convention so the same Y.Text content edits identically on web (via\n * y-prosemirror over a ProseMirror schema) and on mobile (via this library).\n */\nexport type MarkAttrs = Record<string, unknown>;\n\n/**\n * Declaration of a single mark in a {@link Schema}.\n *\n * @property attrs Optional schema for the mark's attributes. Validated on insert.\n * If a mark takes no attributes (e.g. `bold`, `italic`), omit this entirely.\n * @property renderStyle The style applied to spans bearing this mark when the\n * editor renders to native text. Only a subset of RN's `TextStyle` is honoured\n * on native (see {@link RENDERABLE_TEXT_STYLE_KEYS}); other keys are ignored.\n * @property onTap Optional handler invoked when the user taps a span bearing\n * this mark. The classic use case is `link` opening a URL. The handler runs in\n * JS regardless of which platform fired the tap.\n */\nexport interface MarkSpec {\n attrs?: Record<\n string,\n {\n type: 'string' | 'number' | 'boolean';\n required?: boolean;\n default?: unknown;\n }\n >;\n renderStyle?: TextStyle;\n onTap?: (attrs: MarkAttrs) => void;\n}\n\n/**\n * The set of marks an editor instance accepts.\n *\n * The schema is the contract between the consuming application and the editor.\n * Marks not declared here are dropped on insert (with an optional warning, see\n * {@link YTextInputProps.onSchemaViolation}). This is what makes the editor safe\n * for AI-generated content: an LLM cannot smuggle in `<script>`-equivalent\n * marks that the consumer hasn't explicitly opted into.\n */\nexport interface Schema {\n marks: Record<string, MarkSpec>;\n}\n\n/**\n * A contiguous run of text bearing the same formatting marks.\n *\n * This is the wire format used to push attributed text down to the native view.\n * A {@link Y.Text} is converted to `Run[]` via {@link ytextToRuns}; the native\n * side renders each run with the styles declared in its schema's marks.\n */\nexport interface Run {\n text: string;\n marks: MarkAttrs;\n}\n\n/**\n * A character-offset range inside a {@link Y.Text}.\n *\n * Offsets are UTF-16 code units, matching `Y.Text`'s native offsets, RN's\n * `TextInput` selection, iOS `NSRange`, and Android `Editable.getSelectionStart`.\n * A collapsed cursor is represented by `from === to`.\n */\nexport interface SelectionRange {\n from: number;\n to: number;\n}\n\n/**\n * The shape of an edit emitted by the native view when the user types, pastes,\n * or deletes.\n *\n * v0.1 uses a single `replace`-shaped edit that subsumes insert (`from === to`),\n * delete (`text === ''`), and replace. This matches iOS's\n * `textView(_:shouldChangeTextIn:replacementText:)` directly.\n *\n * @internal\n */\nexport interface ReplaceEdit {\n type: 'replace';\n from: number;\n to: number;\n text: string;\n}\n\n/**\n * Props for the {@link YTextInput} editable component.\n */\nexport interface YTextInputProps {\n /** The Y.Text whose content this editor displays and mutates. */\n yText: Y.Text;\n /** The set of marks accepted by this editor. See {@link Schema}. */\n schema: Schema;\n /** RN text style applied as the baseline (font family, size, default color). */\n style?: StyleProp<TextStyle>;\n /** Placeholder text shown when the Y.Text is empty. */\n placeholder?: string;\n /** Colour for the placeholder text. Defaults to a muted grey. */\n placeholderTextColor?: string;\n /** Auto-focus on mount (raises the keyboard). Defaults to `false`. */\n autoFocus?: boolean;\n /** Whether the editor accepts input. Defaults to `true`. */\n editable?: boolean;\n /** Fires whenever the user moves the caret or changes the selected range. */\n onSelectionChange?: (selection: SelectionRange) => void;\n /** Fires when the editor gains focus. */\n onFocus?: () => void;\n /** Fires when the editor loses focus. */\n onBlur?: () => void;\n /**\n * Fires when an insert contains a mark not declared in the schema.\n * The mark is dropped silently regardless; this is your hook to log the\n * violation (e.g. when an LLM generates an unrecognised mark).\n */\n onSchemaViolation?: (info: { mark: string; attrs: MarkAttrs }) => void;\n}\n\n/**\n * Props for the {@link YTextRenderer} read-only component.\n */\nexport interface YTextRendererProps {\n /** The Y.Text to render. */\n yText: Y.Text;\n /** The schema describing how to render each mark. */\n schema: Schema;\n /** RN text style applied as the baseline. */\n style?: StyleProp<TextStyle>;\n /** Maximum lines to render before truncation. */\n numberOfLines?: number;\n}\n\n/**\n * The imperative editor API returned by {@link useYTextEditor}.\n *\n * All commands operate at the current selection unless given explicit ranges.\n * Commands that mutate (`toggleMark`, `insertText`, ...) wrap the mutation in a\n * Y.Doc transaction tagged with the {@link ORIGIN_LOCAL_VIEW} origin so the\n * native view doesn't re-render itself in response.\n */\nexport interface YTextEditor {\n /** Current selection, or `null` if the editor isn't focused. */\n getSelection(): SelectionRange | null;\n /** Move the cursor / select a range. No-op if the editor isn't mounted. */\n setSelection(range: SelectionRange): void;\n\n /**\n * Toggle the mark across the current selection. If the entire selection\n * already has the mark, removes it; otherwise applies it.\n */\n toggleMark(name: string, attrs?: MarkAttrs): void;\n /** Force-apply the mark across the current selection. */\n setMark(name: string, attrs?: MarkAttrs): void;\n /** Remove the mark from the current selection. */\n removeMark(name: string): void;\n /**\n * Marks currently active at the selection's start.\n *\n * For a collapsed selection this is the marks of the character to the left,\n * plus any `pending marks` (marks toggled while the selection was collapsed,\n * which apply to the next character typed).\n */\n marksAtSelection(): MarkAttrs;\n\n /** Insert text at the current selection, replacing any selected range. */\n insertText(text: string, attrs?: MarkAttrs): void;\n /** Delete the given range. Offsets are in UTF-16 code units. */\n deleteRange(from: number, to: number): void;\n\n /** Raise the keyboard / programmatically focus the editor. */\n focus(): void;\n /** Dismiss the keyboard / blur the editor. */\n blur(): void;\n /** Whether the editor currently has keyboard focus. */\n isFocused(): boolean;\n}\n\n/**\n * The Yjs transaction origin used by this library when applying user edits\n * captured from the native view, or programmatic edits from {@link YTextEditor}.\n *\n * Consumers can read `transaction.origin === ORIGIN_LOCAL_VIEW` to distinguish\n * edits coming from the editor itself from edits coming from sync providers\n * (`y-websocket`, your custom backend, etc.). The library uses it internally to\n * avoid re-rendering the view in response to its own writes.\n */\nexport const ORIGIN_LOCAL_VIEW = Symbol('@eclosion-tech/react-native-yjs-text/local-view');\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type * as Y from 'yjs';
|
|
2
|
+
import type { Schema, YTextEditor } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Build a stable imperative editor handle bound to `yText`.
|
|
5
|
+
*
|
|
6
|
+
* Returns the same `YTextEditor` instance across re-renders so that consumers
|
|
7
|
+
* can pass commands to event handlers without re-binding. The commands resolve
|
|
8
|
+
* the *current* live native view at call time via the editor registry — so
|
|
9
|
+
* unmounting and remounting the `YTextInput` doesn't break a toolbar that's
|
|
10
|
+
* holding the editor handle.
|
|
11
|
+
*
|
|
12
|
+
* If no `YTextInput` is currently mounted for this Y.Text, commands that need
|
|
13
|
+
* a live view (focus, selection-dependent mark toggles) gracefully no-op. Pure
|
|
14
|
+
* Y.Text mutations (`insertText` with explicit ranges via `deleteRange`,
|
|
15
|
+
* `setMark` on a known selection) still work — useful for programmatic edits.
|
|
16
|
+
*
|
|
17
|
+
* @param yText The Y.Text this editor mutates.
|
|
18
|
+
* @param schema The schema declaring which marks are valid.
|
|
19
|
+
*/
|
|
20
|
+
export declare function useYTextEditor(yText: Y.Text, schema: Schema): YTextEditor;
|
|
21
|
+
//# sourceMappingURL=useYTextEditor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useYTextEditor.d.ts","sourceRoot":"","sources":["../src/useYTextEditor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,CAAC,MAAM,KAAK,CAAC;AAK9B,OAAO,KAAK,EAAa,MAAM,EAAkB,WAAW,EAAE,MAAM,SAAS,CAAC;AAE9E;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAiJzE"}
|