@canvist/lynx 0.0.0-stage → 0.2.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.
@@ -0,0 +1,31 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "CanvistLynx"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = { type: package["license"] }
11
+ s.author = { "Ifiok Jr." => "support@canvist.dev" }
12
+ s.source = { git: "https://github.com/canvist/canvist.git", tag: "v#{s.version}" }
13
+
14
+ s.platforms = { ios: "17.0" }
15
+ s.source_files = "ios/**/*.{h,m}"
16
+ # Apps register the element by its tag and import only the module.
17
+ s.public_header_files = "ios/CanvistLynxModule.h"
18
+
19
+ # The native editor and its hosting (`CanvistHostView`, `CanvistHostCalls`):
20
+ # `platforms/apple/Canvist.podspec` in the canvist repository, which the
21
+ # app's Podfile points at.
22
+ s.dependency "Canvist", s.version.to_s
23
+ s.dependency "Lynx"
24
+
25
+ # No Intel simulator slice, like `Canvist`, and `Canvist-Swift.h`, which a
26
+ # static Swift pod does not put on its dependents' search paths.
27
+ s.pod_target_xcconfig = {
28
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" => "x86_64",
29
+ "HEADER_SEARCH_PATHS" => '"$(PODS_CONFIGURATION_BUILD_DIR)/Canvist/Swift Compatibility Header"',
30
+ }
31
+ end
@@ -0,0 +1,35 @@
1
+ // The Lynx glue for canvist's Android editor (`io.github.ifiokjr:canvist-android`).
2
+ // The element is Java so Lynx's annotation processor, which generates its
3
+ // attribute setters, runs without kapt; the module is Kotlin, which Lynx
4
+ // reaches by reflection.
5
+ import groovy.json.JsonSlurper
6
+
7
+ plugins {
8
+ id "com.android.library"
9
+ }
10
+
11
+ // The editor is released in step with this package, so they share a version.
12
+ def canvistVersion = new JsonSlurper().parse(file("../package.json")).version
13
+ def lynxVersion = rootProject.findProperty("lynxVersion") ?: "4.1.0"
14
+
15
+ android {
16
+ namespace "io.github.ifiokjr.canvist.lynx"
17
+ compileSdk 37
18
+
19
+ defaultConfig {
20
+ minSdk 24
21
+ }
22
+
23
+ compileOptions {
24
+ sourceCompatibility JavaVersion.VERSION_17
25
+ targetCompatibility JavaVersion.VERSION_17
26
+ }
27
+ }
28
+
29
+ dependencies {
30
+ api "io.github.ifiokjr:canvist-android:${canvistVersion}"
31
+ implementation "androidx.annotation:annotation:1.9.1"
32
+ // The app brings Lynx itself.
33
+ compileOnly "org.lynxsdk.lynx:lynx:${lynxVersion}"
34
+ annotationProcessor "org.lynxsdk.lynx:lynx-processor:${lynxVersion}"
35
+ }
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,33 @@
1
+ package io.github.ifiokjr.canvist.lynx;
2
+
3
+ import com.lynx.tasm.LynxEnv;
4
+ import com.lynx.tasm.behavior.Behavior;
5
+ import com.lynx.tasm.behavior.LynxContext;
6
+ import com.lynx.tasm.behavior.ui.LynxUI;
7
+
8
+ /**
9
+ * Registers canvist with Lynx: call {@link #registerModule()} after
10
+ * {@code LynxEnv.inst().init(...)}, and add {@link #behavior()} to each
11
+ * {@code LynxViewBuilder} with {@code addBehavior}.
12
+ */
13
+ public final class CanvistLynx {
14
+ /** The element's tag. */
15
+ public static final String TAG = "canvist-editor";
16
+
17
+ private CanvistLynx() {}
18
+
19
+ /** Register the {@code CanvistEditors} native module with every LynxView. */
20
+ public static void registerModule() {
21
+ LynxEnv.inst().registerModule(CanvistLynxModule.NAME, CanvistLynxModule.class);
22
+ }
23
+
24
+ /** The behavior that creates {@code <canvist-editor>} elements. */
25
+ public static Behavior behavior() {
26
+ return new Behavior(TAG) {
27
+ @Override
28
+ public LynxUI createUI(LynxContext context) {
29
+ return new CanvistLynxEditor(context);
30
+ }
31
+ };
32
+ }
33
+ }
@@ -0,0 +1,142 @@
1
+ package io.github.ifiokjr.canvist.lynx;
2
+
3
+ import android.content.Context;
4
+ import androidx.annotation.NonNull;
5
+ import com.lynx.react.bridge.Callback;
6
+ import com.lynx.react.bridge.ReadableArray;
7
+ import com.lynx.react.bridge.ReadableMap;
8
+ import com.lynx.tasm.behavior.LynxContext;
9
+ import com.lynx.tasm.behavior.LynxProp;
10
+ import com.lynx.tasm.behavior.LynxUIMethod;
11
+ import com.lynx.tasm.behavior.LynxUIMethodConstants;
12
+ import com.lynx.tasm.behavior.ui.LynxUI;
13
+ import com.lynx.tasm.event.LynxCustomEvent;
14
+ import io.github.ifiokjr.canvist.CanvistHostView;
15
+ import io.github.ifiokjr.canvist.EditorChange;
16
+ import java.util.ArrayList;
17
+ import java.util.List;
18
+
19
+ /**
20
+ * {@code <canvist-editor>}: a {@link CanvistHostView} created from the
21
+ * element's first attributes, sending {@code ready}, {@code change},
22
+ * {@code focuschange}, and {@code error} events.
23
+ */
24
+ public class CanvistLynxEditor extends LynxUI<CanvistHostView> implements CanvistHostView.Listener {
25
+ private String editorId;
26
+ private List<String> extensions = new ArrayList<>();
27
+ private String contentFormat;
28
+ private String content;
29
+
30
+ public CanvistLynxEditor(LynxContext context) {
31
+ super(context);
32
+ }
33
+
34
+ @Override
35
+ protected CanvistHostView createView(Context context) {
36
+ CanvistHostView host = new CanvistHostView(context);
37
+ host.setListener(this);
38
+ return host;
39
+ }
40
+
41
+ @LynxProp(name = "editor-id")
42
+ public void setEditorId(String value) {
43
+ editorId = value;
44
+ }
45
+
46
+ @LynxProp(name = "extensions")
47
+ public void setExtensions(ReadableArray value) {
48
+ List<String> names = new ArrayList<>();
49
+ for (int index = 0; value != null && index < value.size(); index++) {
50
+ names.add(value.getString(index));
51
+ }
52
+ extensions = names;
53
+ }
54
+
55
+ @LynxProp(name = "content-format")
56
+ public void setContentFormat(String value) {
57
+ contentFormat = value;
58
+ }
59
+
60
+ @LynxProp(name = "content")
61
+ public void setContent(String value) {
62
+ content = value;
63
+ }
64
+
65
+ @LynxProp(name = "text-size")
66
+ public void setTextSize(float value) {
67
+ mView.setFontSize(value);
68
+ }
69
+
70
+ /**
71
+ * The editor starts once the first attributes have all arrived; later
72
+ * changes to {@code extensions} and {@code content} have no effect, as the
73
+ * JavaScript API documents.
74
+ */
75
+ @Override
76
+ public void onPropsUpdated() {
77
+ super.onPropsUpdated();
78
+
79
+ if (editorId == null || editorId.isEmpty()) {
80
+ return;
81
+ }
82
+
83
+ mView.start(editorId, extensions, emptyAsNull(contentFormat), emptyAsNull(content));
84
+ }
85
+
86
+ @Override
87
+ public void destroy() {
88
+ mView.stop();
89
+ super.destroy();
90
+ }
91
+
92
+ @LynxUIMethod
93
+ public void focus(ReadableMap params, Callback callback) {
94
+ mView.focusEditor();
95
+ callback.invoke(LynxUIMethodConstants.SUCCESS);
96
+ }
97
+
98
+ @LynxUIMethod
99
+ public void blur(ReadableMap params, Callback callback) {
100
+ mView.blurEditor();
101
+ callback.invoke(LynxUIMethodConstants.SUCCESS);
102
+ }
103
+
104
+ @Override
105
+ public void onReady(@NonNull CanvistHostView view, @NonNull String editorId) {
106
+ LynxCustomEvent event = new LynxCustomEvent(getSign(), "ready");
107
+ event.addDetail("editorId", editorId);
108
+ emit(event);
109
+ }
110
+
111
+ @Override
112
+ public void onChange(@NonNull CanvistHostView view, @NonNull EditorChange change) {
113
+ LynxCustomEvent event = new LynxCustomEvent(getSign(), "change");
114
+ event.addDetail("document", change.getDocument());
115
+ event.addDetail("selection", change.getSelection());
116
+ event.addDetail("composition", change.getComposition());
117
+ emit(event);
118
+ }
119
+
120
+ @Override
121
+ public void onFocusChange(@NonNull CanvistHostView view, boolean focused) {
122
+ LynxCustomEvent event = new LynxCustomEvent(getSign(), "focuschange");
123
+ event.addDetail("focused", focused);
124
+ emit(event);
125
+ }
126
+
127
+ @Override
128
+ public void onError(@NonNull CanvistHostView view, @NonNull String code, @NonNull String message) {
129
+ LynxCustomEvent event = new LynxCustomEvent(getSign(), "error");
130
+ event.addDetail("code", code);
131
+ event.addDetail("message", message);
132
+ emit(event);
133
+ }
134
+
135
+ private void emit(LynxCustomEvent event) {
136
+ getLynxContext().getEventEmitter().sendCustomEvent(event);
137
+ }
138
+
139
+ private static String emptyAsNull(String value) {
140
+ return value == null || value.isEmpty() ? null : value;
141
+ }
142
+ }
@@ -0,0 +1,109 @@
1
+ package io.github.ifiokjr.canvist.lynx
2
+
3
+ import android.content.Context
4
+ import com.lynx.jsbridge.LynxMethod
5
+ import com.lynx.jsbridge.LynxModule
6
+ import io.github.ifiokjr.canvist.Command
7
+ import io.github.ifiokjr.canvist.Editor
8
+ import io.github.ifiokjr.canvist.HostedEditorException
9
+ import io.github.ifiokjr.canvist.HostedEditors
10
+ import io.github.ifiokjr.canvist.docFormat
11
+ import io.github.ifiokjr.canvist.selectRangeJson
12
+ import io.github.ifiokjr.canvist.selectionJson
13
+ import org.json.JSONObject
14
+
15
+ /**
16
+ * The `CanvistEditors` native module: synchronous calls, on the JavaScript
17
+ * thread, into the editors of `<canvist-editor>` elements. Every method
18
+ * answers with a JSON envelope, `{"value": ...}` or `{"error": message}`,
19
+ * because Lynx reports native exceptions to the host rather than to
20
+ * JavaScript.
21
+ */
22
+ class CanvistLynxModule(
23
+ context: Context,
24
+ ) : LynxModule(context) {
25
+ @LynxMethod
26
+ fun run(
27
+ editorId: String,
28
+ name: String,
29
+ args: String,
30
+ ): String = reply(editorId) { it.run(Command(name, args)).json() }
31
+
32
+ @LynxMethod
33
+ fun can(
34
+ editorId: String,
35
+ name: String,
36
+ args: String,
37
+ ): String = reply(editorId) { it.can(Command(name, args)).json() }
38
+
39
+ @LynxMethod
40
+ fun query(
41
+ editorId: String,
42
+ name: String,
43
+ args: String,
44
+ ): String = reply(editorId) { it.query(name, args) }
45
+
46
+ @LynxMethod
47
+ fun load(
48
+ editorId: String,
49
+ format: String,
50
+ content: String,
51
+ ): String =
52
+ reply(editorId) {
53
+ it.load(docFormat(format), content)
54
+ "true"
55
+ }
56
+
57
+ @LynxMethod
58
+ fun exportDocument(
59
+ editorId: String,
60
+ format: String,
61
+ ): String = reply(editorId) { JSONObject.quote(it.export(docFormat(format))) }
62
+
63
+ @LynxMethod
64
+ fun selection(editorId: String): String = reply(editorId) { it.selectionJson() }
65
+
66
+ @LynxMethod
67
+ fun select(
68
+ editorId: String,
69
+ range: String,
70
+ ): String = reply(editorId) { it.selectRangeJson(range).json() }
71
+
72
+ @LynxMethod
73
+ fun selectAll(editorId: String): String = reply(editorId) { it.selectAll().json() }
74
+
75
+ @LynxMethod
76
+ fun insertText(
77
+ editorId: String,
78
+ text: String,
79
+ ): String = reply(editorId) { it.insertText(text).json() }
80
+
81
+ @LynxMethod
82
+ fun undo(editorId: String): String = reply(editorId) { it.undo().json() }
83
+
84
+ @LynxMethod
85
+ fun redo(editorId: String): String = reply(editorId) { it.redo().json() }
86
+
87
+ @LynxMethod
88
+ fun canUndo(editorId: String): String = reply(editorId) { it.canUndo().json() }
89
+
90
+ @LynxMethod
91
+ fun canRedo(editorId: String): String = reply(editorId) { it.canRedo().json() }
92
+
93
+ companion object {
94
+ const val NAME = "CanvistEditors"
95
+ }
96
+ }
97
+
98
+ /** The envelope for [body]'s JSON answer on the editor [editorId] names, or for its failure. */
99
+ private fun reply(
100
+ editorId: String,
101
+ body: (Editor) -> String,
102
+ ): String =
103
+ try {
104
+ "{\"value\":${HostedEditors.call(editorId, body)}}"
105
+ } catch (error: HostedEditorException) {
106
+ "{\"error\":${JSONObject.quote(error.message)}}"
107
+ }
108
+
109
+ private fun Boolean.json() = if (this) "true" else "false"
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The bridge from the typed editor to one `<canvist-editor>`: synchronous
3
+ * calls through the `CanvistEditors` native module, and focus through the
4
+ * element's UI methods.
5
+ *
6
+ * @module
7
+ */
8
+ import type { NativeBridge } from "@canvist/headless/native";
9
+ import type { NodesRef } from "@lynx-js/types";
10
+ import type { CanvistEditorsModule } from "./native.ts";
11
+ /**
12
+ * A bridge to the editor `editorId` names through `module`, focusing the
13
+ * element `element` returns. Focus calls do nothing once the element is gone.
14
+ */
15
+ export declare function nativeBridge(module: CanvistEditorsModule, editorId: string, element: () => NodesRef | null): NativeBridge;
package/dist/bridge.js ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A bridge to the editor `editorId` names through `module`, focusing the
3
+ * element `element` returns. Focus calls do nothing once the element is gone.
4
+ */
5
+ export function nativeBridge(module, editorId, element) {
6
+ const invoke = (method) => {
7
+ element()?.invoke({ method }).exec();
8
+ };
9
+ return {
10
+ run: (name, args) => boolean(module.run(editorId, name, args)),
11
+ can: (name, args) => boolean(module.can(editorId, name, args)),
12
+ query: (name, args) => JSON.stringify(value(module.query(editorId, name, args))),
13
+ load: (format, content) => {
14
+ value(module.load(editorId, format, content));
15
+ },
16
+ export: (format) => string(module.exportDocument(editorId, format)),
17
+ selection: () => JSON.stringify(value(module.selection(editorId))),
18
+ select: (range) => {
19
+ value(module.select(editorId, range));
20
+ },
21
+ selectAll: () => {
22
+ value(module.selectAll(editorId));
23
+ },
24
+ insertText: (text) => boolean(module.insertText(editorId, text)),
25
+ undo: () => boolean(module.undo(editorId)),
26
+ redo: () => boolean(module.redo(editorId)),
27
+ canUndo: () => boolean(module.canUndo(editorId)),
28
+ canRedo: () => boolean(module.canRedo(editorId)),
29
+ focus: () => invoke("focus"),
30
+ blur: () => invoke("blur"),
31
+ };
32
+ }
33
+ /** The value in `reply`, throwing its error instead when it has one. */
34
+ function value(reply) {
35
+ const parsed = JSON.parse(reply);
36
+ if (!isReply(parsed)) {
37
+ throw new Error(`[canvist:editor] the native module answered ${reply}`);
38
+ }
39
+ if ("error" in parsed) {
40
+ throw new Error(parsed.error);
41
+ }
42
+ return parsed.value;
43
+ }
44
+ function boolean(reply) {
45
+ const answer = value(reply);
46
+ if (typeof answer !== "boolean") {
47
+ throw new Error(`[canvist:editor] expected a boolean, not ${reply}`);
48
+ }
49
+ return answer;
50
+ }
51
+ function string(reply) {
52
+ const answer = value(reply);
53
+ if (typeof answer !== "string") {
54
+ throw new Error(`[canvist:editor] expected a string, not ${reply}`);
55
+ }
56
+ return answer;
57
+ }
58
+ function isReply(reply) {
59
+ if (typeof reply !== "object" || reply === null) {
60
+ return false;
61
+ }
62
+ return "value" in reply ||
63
+ ("error" in reply && typeof reply.error === "string");
64
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The editor component and hooks.
3
+ *
4
+ * @module
5
+ */
6
+ import type { CanvistError, Content, StarterKit } from "@canvist/headless";
7
+ import { NativeEditor } from "@canvist/headless/native";
8
+ import { type ReactNode } from "@lynx-js/react";
9
+ import type { StandardProps } from "@lynx-js/types";
10
+ import type { CanvistEditorElementProps } from "./native.ts";
11
+ /** The extensions the native editors compile in: TipTap's StarterKit, or some of it. */
12
+ export type NativeExtensions = readonly StarterKit[number][];
13
+ /**
14
+ * A native editor whose commands, queries, and documents are typed from
15
+ * the extensions `E`, with the same API as `@canvist/web`'s editor.
16
+ */
17
+ export type Editor<E extends NativeExtensions> = NativeEditor<E>;
18
+ /** Options for {@link useCanvistEditor} and {@link CanvistEditor}. */
19
+ export interface EditorOptions<E extends NativeExtensions> {
20
+ /** The extensions to register: every StarterKit extension by default. Read once. */
21
+ readonly extensions?: E;
22
+ /** The document to start with: one empty paragraph by default. Read once. */
23
+ readonly content?: Content<E>;
24
+ /** The body text size before the user's text size setting; the platform's body size by default. */
25
+ readonly textSize?: number;
26
+ /** Called when the element could not create its editor or load `content`. */
27
+ readonly onError?: (error: CanvistError) => void;
28
+ }
29
+ /**
30
+ * Props for {@link CanvistView}: what {@link useCanvistEditor} returns, plus
31
+ * the element's own, such as `style` and `accessibility-label`.
32
+ */
33
+ export type CanvistViewProps = CanvistEditorElementProps;
34
+ /** What {@link useCanvistEditor} returns. */
35
+ export interface CanvistEditorHandle<E extends NativeExtensions> {
36
+ /** The editor, once the element has created it; `null` before that. */
37
+ readonly editor: Editor<E> | null;
38
+ /** Spread onto a {@link CanvistView}, alongside its style and accessibility props. */
39
+ readonly props: CanvistViewProps;
40
+ }
41
+ /** The `<canvist-editor>` element. Give it a size, as it scrolls its own document. */
42
+ export declare function CanvistView(props: CanvistViewProps): ReactNode;
43
+ /**
44
+ * Create an editor in the {@link CanvistView} you spread `props` onto.
45
+ * `extensions` and `content` are read when the element is created; later
46
+ * changes to them have no effect.
47
+ *
48
+ * ```tsx
49
+ * function Notes() {
50
+ * const { editor, props } = useCanvistEditor({ extensions: [bold, heading] });
51
+ *
52
+ * return (
53
+ * <view>
54
+ * <text bindtap={() => editor?.commands.toggleBold()}>Bold</text>
55
+ * <CanvistView {...props} style={{ flex: 1 }} />
56
+ * </view>
57
+ * );
58
+ * }
59
+ * ```
60
+ */
61
+ export declare function useCanvistEditor<const E extends NativeExtensions = StarterKit>(options?: EditorOptions<E>): CanvistEditorHandle<E>;
62
+ /**
63
+ * Read `select(editor)` and re-render whenever it changes: after any edit,
64
+ * selection change, or change of focus. `select` must return a primitive
65
+ * or a value that stays the same object while nothing changed, as with
66
+ * `useSyncExternalStore`. Until the editor exists, the value is `fallback`.
67
+ */
68
+ export declare function useEditorState<E extends NativeExtensions, T>(editor: Editor<E> | null, select: (editor: Editor<E>) => T, fallback: T): T;
69
+ /** Props for {@link CanvistEditor}: the editor's options and the element's. */
70
+ export interface CanvistEditorProps<E extends NativeExtensions> extends EditorOptions<E>, StandardProps {
71
+ /** Called once the editor is ready. */
72
+ readonly onReady?: (editor: Editor<E>) => void;
73
+ /** Called after every change to the document, the selection, or focus. */
74
+ readonly onUpdate?: (editor: Editor<E>) => void;
75
+ }
76
+ /**
77
+ * A native canvist editor: the iOS or Android `CanvistView`. Give it a
78
+ * size, as it scrolls its own document.
79
+ *
80
+ * ```tsx
81
+ * <CanvistEditor
82
+ * content={{ format: "markdown", input: "# Notes" }}
83
+ * onUpdate={(editor) => save(editor.export("markdown"))}
84
+ * style={{ flex: 1 }}
85
+ * />
86
+ * ```
87
+ */
88
+ export declare function CanvistEditor<const E extends NativeExtensions = StarterKit>(props: CanvistEditorProps<E>): ReactNode;
@@ -0,0 +1,145 @@
1
+ import { starterKit } from "@canvist/headless/extensions";
2
+ import { nativeCanvistError, NativeEditor } from "@canvist/headless/native";
3
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "@lynx-js/react";
4
+ import { nativeBridge } from "./bridge.js";
5
+ /** The `<canvist-editor>` element. Give it a size, as it scrolls its own document. */
6
+ export function CanvistView(props) {
7
+ return <canvist-editor {...props}/>;
8
+ }
9
+ let editors = 0;
10
+ /** A key for one element's editor, unique in this JavaScript runtime. */
11
+ function nextEditorId() {
12
+ editors += 1;
13
+ return `canvist-${editors}`;
14
+ }
15
+ /**
16
+ * Create an editor in the {@link CanvistView} you spread `props` onto.
17
+ * `extensions` and `content` are read when the element is created; later
18
+ * changes to them have no effect.
19
+ *
20
+ * ```tsx
21
+ * function Notes() {
22
+ * const { editor, props } = useCanvistEditor({ extensions: [bold, heading] });
23
+ *
24
+ * return (
25
+ * <view>
26
+ * <text bindtap={() => editor?.commands.toggleBold()}>Bold</text>
27
+ * <CanvistView {...props} style={{ flex: 1 }} />
28
+ * </view>
29
+ * );
30
+ * }
31
+ * ```
32
+ */
33
+ export function useCanvistEditor(options = {}) {
34
+ const [editorId] = useState(nextEditorId);
35
+ const [editor, setEditor] = useState(null);
36
+ const initial = useRef(options);
37
+ const element = useRef(null);
38
+ // Events arrive through callbacks made once, so they read the editor here.
39
+ const current = useRef(null);
40
+ const onError = useLatest(options.onError);
41
+ const bindready = useCallback(() => {
42
+ const module = NativeModules.CanvistEditors;
43
+ if (module === undefined) {
44
+ reportError(nativeCanvistError("config", "the CanvistEditors native module is not registered; see @canvist/lynx's setup"), onError.current);
45
+ return;
46
+ }
47
+ // Without extensions, `E` is the default: the whole StarterKit.
48
+ const extensions = (initial.current.extensions ?? starterKit);
49
+ const created = new NativeEditor(nativeBridge(module, editorId, () => element.current), extensions);
50
+ current.current = created;
51
+ setEditor(created);
52
+ }, [editorId, onError]);
53
+ const bindchange = useCallback(() => {
54
+ current.current?.notifyChange();
55
+ }, []);
56
+ const bindfocuschange = useCallback((event) => {
57
+ current.current?.notifyFocus(event.detail.focused);
58
+ }, []);
59
+ const binderror = useCallback((event) => {
60
+ const { code, message } = event.detail;
61
+ reportError(nativeCanvistError(code, message), onError.current);
62
+ }, [onError]);
63
+ const { textSize } = options;
64
+ const props = useMemo(() => {
65
+ const { extensions = starterKit, content } = initial.current;
66
+ return {
67
+ ref: element,
68
+ "editor-id": editorId,
69
+ extensions: extensions.map((extension) => extension.name),
70
+ ...documentProps(content),
71
+ ...(textSize === undefined ? {} : { "text-size": textSize }),
72
+ bindready,
73
+ bindchange,
74
+ bindfocuschange,
75
+ binderror,
76
+ };
77
+ }, [editorId, textSize, bindready, bindchange, bindfocuschange, binderror]);
78
+ return { editor, props };
79
+ }
80
+ /**
81
+ * Read `select(editor)` and re-render whenever it changes: after any edit,
82
+ * selection change, or change of focus. `select` must return a primitive
83
+ * or a value that stays the same object while nothing changed, as with
84
+ * `useSyncExternalStore`. Until the editor exists, the value is `fallback`.
85
+ */
86
+ export function useEditorState(editor, select, fallback) {
87
+ const subscribe = useCallback((listener) => editor?.subscribe(listener) ?? noop, [editor]);
88
+ return useSyncExternalStore(subscribe, () => editor === null ? fallback : select(editor), () => fallback);
89
+ }
90
+ /**
91
+ * A native canvist editor: the iOS or Android `CanvistView`. Give it a
92
+ * size, as it scrolls its own document.
93
+ *
94
+ * ```tsx
95
+ * <CanvistEditor
96
+ * content={{ format: "markdown", input: "# Notes" }}
97
+ * onUpdate={(editor) => save(editor.export("markdown"))}
98
+ * style={{ flex: 1 }}
99
+ * />
100
+ * ```
101
+ */
102
+ export function CanvistEditor(props) {
103
+ const { extensions, content, textSize, onError, onReady, onUpdate, ...element } = props;
104
+ const { editor, props: editorProps } = useCanvistEditor({
105
+ ...(extensions === undefined ? {} : { extensions }),
106
+ ...(content === undefined ? {} : { content }),
107
+ ...(textSize === undefined ? {} : { textSize }),
108
+ ...(onError === undefined ? {} : { onError }),
109
+ });
110
+ const callbacks = useLatest({ onReady, onUpdate });
111
+ useEffect(() => {
112
+ if (editor === null) {
113
+ return;
114
+ }
115
+ callbacks.current.onReady?.(editor);
116
+ return editor.subscribe(() => callbacks.current.onUpdate?.(editor));
117
+ }, [editor, callbacks]);
118
+ return <CanvistView {...element} {...editorProps}/>;
119
+ }
120
+ /** The element attributes for the starting document. */
121
+ function documentProps(content) {
122
+ if (content === undefined) {
123
+ return {};
124
+ }
125
+ return "format" in content
126
+ ? { "content-format": content.format, content: content.input }
127
+ : { "content-format": "json", content: JSON.stringify(content) };
128
+ }
129
+ /** A ref that always holds the latest `value`. */
130
+ function useLatest(value) {
131
+ const ref = useRef(value);
132
+ useEffect(() => {
133
+ ref.current = value;
134
+ });
135
+ return ref;
136
+ }
137
+ function reportError(error, onError) {
138
+ if (onError === undefined) {
139
+ console.error(error);
140
+ }
141
+ else {
142
+ onError(error);
143
+ }
144
+ }
145
+ function noop() { }
package/dist/mod.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The canvist rich-text editor for Lynx.
3
+ *
4
+ * `CanvistEditor` shows the native iOS or Android editor as a
5
+ * `<canvist-editor>` element: canvist's Rust engine, painted natively and
6
+ * edited through the platform's own text input. Commands, queries, and
7
+ * documents are typed from the extensions you register, with the same API
8
+ * as `@canvist/web` and `@canvist/react-native`: calling a command you did
9
+ * not register is a type error. Calls are synchronous.
10
+ *
11
+ * ```tsx
12
+ * import { CanvistEditor, bold, heading } from "@canvist/lynx";
13
+ *
14
+ * <CanvistEditor
15
+ * extensions={[bold, heading]}
16
+ * content={{ format: "markdown", input: "# Notes" }}
17
+ * onReady={(editor) => editor.commands.toggleHeading({ level: 2 })}
18
+ * style={{ flex: 1 }}
19
+ * />
20
+ * ```
21
+ *
22
+ * @module
23
+ */
24
+ export type { CanvistError, CommandName, CommandParams, Commands, Content, DocJson, ErrorCode, ExtensionDescriptor, Extensions, Format, Position, Queries, QueryName, QueryParams, QueryResult, Range, Registered, StarterKit, } from "@canvist/headless";
25
+ export * from "@canvist/headless/extensions";
26
+ export { isCanvistError } from "@canvist/headless/native";
27
+ export { CanvistEditor, type CanvistEditorHandle, type CanvistEditorProps, CanvistView, type CanvistViewProps, type Editor, type EditorOptions, type NativeExtensions, useCanvistEditor, useEditorState, } from "./editor.tsx";
28
+ export type { CanvistEditorElementProps, CanvistEditorsModule, EditorChangeDetail, EditorErrorDetail, EditorFocusDetail, EditorReadyDetail, } from "./native.ts";
package/dist/mod.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "@canvist/headless/extensions";
2
+ export { isCanvistError } from "@canvist/headless/native";
3
+ export { CanvistEditor, CanvistView, useCanvistEditor, useEditorState, } from "./editor.jsx";
@@ -0,0 +1,80 @@
1
+ /**
2
+ * What the native side of `@canvist/lynx` provides: the `<canvist-editor>`
3
+ * element and the `CanvistEditors` native module, typed for ReactLynx.
4
+ *
5
+ * @module
6
+ */
7
+ import type { BaseEvent, StandardProps } from "@lynx-js/types";
8
+ /** What changed, after every change to the document or selection. */
9
+ export interface EditorChangeDetail {
10
+ readonly document: boolean;
11
+ readonly selection: boolean;
12
+ readonly composition: boolean;
13
+ }
14
+ /** The element gained or lost focus. */
15
+ export interface EditorFocusDetail {
16
+ readonly focused: boolean;
17
+ }
18
+ /** The element could not create its editor or load its content. */
19
+ export interface EditorErrorDetail {
20
+ readonly code: string;
21
+ readonly message: string;
22
+ }
23
+ /** The element's editor is ready for calls through `CanvistEditors`. */
24
+ export interface EditorReadyDetail {
25
+ readonly editorId: string;
26
+ }
27
+ /** The attributes and events of `<canvist-editor>`. */
28
+ export interface CanvistEditorElementProps extends StandardProps {
29
+ /** The key the `CanvistEditors` module finds this element's editor by. */
30
+ "editor-id": string;
31
+ /** The extensions to register, by name. Read when the element is created. */
32
+ extensions: readonly string[];
33
+ /** The format of `content`. Read when the element is created. */
34
+ "content-format"?: string;
35
+ /** The document to start with. Read when the element is created. */
36
+ content?: string;
37
+ /** The body text size before the user's text size setting; 0 is the platform's default. */
38
+ "text-size"?: number;
39
+ bindready?: (event: BaseEvent<"ready", EditorReadyDetail>) => void;
40
+ bindchange?: (event: BaseEvent<"change", EditorChangeDetail>) => void;
41
+ bindfocuschange?: (event: BaseEvent<"focuschange", EditorFocusDetail>) => void;
42
+ binderror?: (event: BaseEvent<"error", EditorErrorDetail>) => void;
43
+ }
44
+ /**
45
+ * The `CanvistEditors` native module. Every method is synchronous and
46
+ * answers with a JSON envelope: `{"value": ...}`, or `{"error": message}`
47
+ * with `[canvist:<code>] <reason>` in the message, because Lynx reports
48
+ * native exceptions to the host rather than to JavaScript.
49
+ */
50
+ export interface CanvistEditorsModule {
51
+ run(editorId: string, name: string, args: string): string;
52
+ can(editorId: string, name: string, args: string): string;
53
+ query(editorId: string, name: string, args: string): string;
54
+ load(editorId: string, format: string, content: string): string;
55
+ exportDocument(editorId: string, format: string): string;
56
+ selection(editorId: string): string;
57
+ select(editorId: string, range: string): string;
58
+ selectAll(editorId: string): string;
59
+ insertText(editorId: string, text: string): string;
60
+ undo(editorId: string): string;
61
+ redo(editorId: string): string;
62
+ canUndo(editorId: string): string;
63
+ canRedo(editorId: string): string;
64
+ }
65
+ declare module "@lynx-js/types" {
66
+ interface IntrinsicElements {
67
+ "canvist-editor": CanvistEditorElementProps;
68
+ }
69
+ interface NativeModules {
70
+ /** Registered by the app with `@canvist/lynx`'s native setup. */
71
+ CanvistEditors?: CanvistEditorsModule;
72
+ }
73
+ }
74
+ declare module "react" {
75
+ namespace JSX {
76
+ interface IntrinsicElements {
77
+ "canvist-editor": CanvistEditorElementProps;
78
+ }
79
+ }
80
+ }
package/dist/native.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ #import <Lynx/LynxUI.h>
2
+
3
+ #import "CanvistSwift.h"
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ /// `<canvist-editor>`: a `CanvistHostView` created from the element's first
8
+ /// attributes, sending `ready`, `change`, `focuschange`, and `error` events.
9
+ @interface CanvistLynxEditor : LynxUI <CanvistHostView *>
10
+ @end
11
+
12
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,109 @@
1
+ #import "CanvistLynxEditor.h"
2
+
3
+ #import <Lynx/LynxComponentRegistry.h>
4
+ #import <Lynx/LynxPropsProcessor.h>
5
+ #import <Lynx/LynxUIMethodProcessor.h>
6
+
7
+ #import "CanvistSwift.h"
8
+
9
+ @implementation CanvistLynxEditor {
10
+ NSString *_editorId;
11
+ NSArray<NSString *> *_extensions;
12
+ NSString *_contentFormat;
13
+ NSString *_content;
14
+ }
15
+
16
+ LYNX_LAZY_REGISTER_UI("canvist-editor")
17
+
18
+ - (CanvistHostView *)createView
19
+ {
20
+ CanvistHostView *host = [CanvistHostView new];
21
+ __weak CanvistLynxEditor *weakSelf = self;
22
+ host.onReady = ^(NSString *editorId) {
23
+ [weakSelf emit:@"ready" detail:@{@"editorId" : editorId}];
24
+ };
25
+ host.onChange = ^(BOOL document, BOOL selection, BOOL composition) {
26
+ [weakSelf emit:@"change" detail:@{@"document" : @(document), @"selection" : @(selection), @"composition" : @(composition)}];
27
+ };
28
+ host.onFocusChange = ^(BOOL focused) {
29
+ [weakSelf emit:@"focuschange" detail:@{@"focused" : @(focused)}];
30
+ };
31
+ host.onError = ^(NSString *code, NSString *message) {
32
+ [weakSelf emit:@"error" detail:@{@"code" : code, @"message" : message}];
33
+ };
34
+
35
+ return host;
36
+ }
37
+
38
+ - (void)dealloc
39
+ {
40
+ [self.view stop];
41
+ }
42
+
43
+ LYNX_PROP_SETTER("editor-id", setEditorId, NSString *)
44
+ {
45
+ _editorId = value;
46
+ }
47
+
48
+ LYNX_PROP_SETTER("extensions", setExtensions, NSArray *)
49
+ {
50
+ NSMutableArray<NSString *> *names = [NSMutableArray arrayWithCapacity:value.count];
51
+ for (id name in value) {
52
+ if ([name isKindOfClass:NSString.class]) {
53
+ [names addObject:name];
54
+ }
55
+ }
56
+ _extensions = names;
57
+ }
58
+
59
+ LYNX_PROP_SETTER("content-format", setContentFormat, NSString *)
60
+ {
61
+ _contentFormat = value;
62
+ }
63
+
64
+ LYNX_PROP_SETTER("content", setContent, NSString *)
65
+ {
66
+ _content = value;
67
+ }
68
+
69
+ LYNX_PROP_SETTER("text-size", setTextSize, CGFloat)
70
+ {
71
+ self.view.fontSize = value;
72
+ }
73
+
74
+ // The editor starts once the first attributes have all arrived; later
75
+ // changes to `extensions` and `content` have no effect, as the JavaScript API
76
+ // documents.
77
+ - (void)propsDidUpdate
78
+ {
79
+ [super propsDidUpdate];
80
+
81
+ if (_editorId.length == 0) {
82
+ return;
83
+ }
84
+
85
+ [self.view startWithEditorId:_editorId
86
+ extensions:_extensions ?: @[]
87
+ format:_contentFormat.length > 0 ? _contentFormat : nil
88
+ content:_content.length > 0 ? _content : nil];
89
+ }
90
+
91
+ LYNX_UI_METHOD(focus)
92
+ {
93
+ [self.view focus];
94
+ callback(kUIMethodSuccess, nil);
95
+ }
96
+
97
+ LYNX_UI_METHOD(blur)
98
+ {
99
+ [self.view blur];
100
+ callback(kUIMethodSuccess, nil);
101
+ }
102
+
103
+ - (void)emit:(NSString *)name detail:(NSDictionary *)detail
104
+ {
105
+ LynxDetailEvent *event = [[LynxDetailEvent alloc] initWithName:name targetSign:[self sign] detail:detail];
106
+ [self.context.eventEmitter dispatchCustomEvent:event];
107
+ }
108
+
109
+ @end
@@ -0,0 +1,14 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <Lynx/LynxModule.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ /// The `CanvistEditors` native module: synchronous calls, on the JavaScript
7
+ /// thread, into the editors of `<canvist-editor>` elements. Every method
8
+ /// answers with a JSON envelope, `{"value": ...}` or `{"error": message}`,
9
+ /// because Lynx reports native exceptions to the host rather than to
10
+ /// JavaScript. Register it with `[config registerModule:CanvistLynxModule.class]`.
11
+ @interface CanvistLynxModule : NSObject <LynxModule>
12
+ @end
13
+
14
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,136 @@
1
+ #import "CanvistLynxModule.h"
2
+
3
+ #import "CanvistSwift.h"
4
+
5
+ /// `text` as a JSON string literal.
6
+ static NSString *JSONString(NSString *text)
7
+ {
8
+ NSData *data = [NSJSONSerialization dataWithJSONObject:text options:NSJSONWritingFragmentsAllowed error:nil];
9
+ return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
10
+ }
11
+
12
+ /// The envelope for `json`, a JSON value, or for `error` when the call failed.
13
+ static NSString *Reply(NSString *_Nullable json, NSError *_Nullable error)
14
+ {
15
+ if (json == nil) {
16
+ return [NSString stringWithFormat:@"{\"error\":%@}", JSONString(error.localizedDescription ?: @"[canvist:editor] unknown failure")];
17
+ }
18
+
19
+ return [NSString stringWithFormat:@"{\"value\":%@}", json];
20
+ }
21
+
22
+ static NSString *_Nullable JSONBool(NSNumber *_Nullable value)
23
+ {
24
+ return value == nil ? nil : (value.boolValue ? @"true" : @"false");
25
+ }
26
+
27
+ static NSString *_Nullable JSONText(NSString *_Nullable value)
28
+ {
29
+ return value == nil ? nil : JSONString(value);
30
+ }
31
+
32
+ @implementation CanvistLynxModule
33
+
34
+ + (NSString *)name
35
+ {
36
+ return @"CanvistEditors";
37
+ }
38
+
39
+ + (NSDictionary<NSString *, NSString *> *)methodLookup
40
+ {
41
+ return @{
42
+ @"run" : NSStringFromSelector(@selector(run:name:args:)),
43
+ @"can" : NSStringFromSelector(@selector(can:name:args:)),
44
+ @"query" : NSStringFromSelector(@selector(query:name:args:)),
45
+ @"load" : NSStringFromSelector(@selector(load:format:content:)),
46
+ @"exportDocument" : NSStringFromSelector(@selector(exportDocument:format:)),
47
+ @"selection" : NSStringFromSelector(@selector(selection:)),
48
+ @"select" : NSStringFromSelector(@selector(select:range:)),
49
+ @"selectAll" : NSStringFromSelector(@selector(selectAll:)),
50
+ @"insertText" : NSStringFromSelector(@selector(insertText:text:)),
51
+ @"undo" : NSStringFromSelector(@selector(undo:)),
52
+ @"redo" : NSStringFromSelector(@selector(redo:)),
53
+ @"canUndo" : NSStringFromSelector(@selector(canUndo:)),
54
+ @"canRedo" : NSStringFromSelector(@selector(canRedo:)),
55
+ };
56
+ }
57
+
58
+ - (NSString *)run:(NSString *)editorId name:(NSString *)name args:(NSString *)args
59
+ {
60
+ NSError *error = nil;
61
+ return Reply(JSONBool([CanvistHostCalls runWithId:editorId name:name args:args error:&error]), error);
62
+ }
63
+
64
+ - (NSString *)can:(NSString *)editorId name:(NSString *)name args:(NSString *)args
65
+ {
66
+ NSError *error = nil;
67
+ return Reply(JSONBool([CanvistHostCalls canWithId:editorId name:name args:args error:&error]), error);
68
+ }
69
+
70
+ - (NSString *)query:(NSString *)editorId name:(NSString *)name args:(NSString *)args
71
+ {
72
+ NSError *error = nil;
73
+ return Reply([CanvistHostCalls queryWithId:editorId name:name args:args error:&error], error);
74
+ }
75
+
76
+ - (NSString *)load:(NSString *)editorId format:(NSString *)format content:(NSString *)content
77
+ {
78
+ NSError *error = nil;
79
+ return Reply(JSONBool([CanvistHostCalls loadWithId:editorId format:format content:content error:&error]), error);
80
+ }
81
+
82
+ - (NSString *)exportDocument:(NSString *)editorId format:(NSString *)format
83
+ {
84
+ NSError *error = nil;
85
+ return Reply(JSONText([CanvistHostCalls exportWithId:editorId format:format error:&error]), error);
86
+ }
87
+
88
+ - (NSString *)selection:(NSString *)editorId
89
+ {
90
+ NSError *error = nil;
91
+ return Reply([CanvistHostCalls selectionWithId:editorId error:&error], error);
92
+ }
93
+
94
+ - (NSString *)select:(NSString *)editorId range:(NSString *)range
95
+ {
96
+ NSError *error = nil;
97
+ return Reply(JSONBool([CanvistHostCalls selectWithId:editorId range:range error:&error]), error);
98
+ }
99
+
100
+ - (NSString *)selectAll:(NSString *)editorId
101
+ {
102
+ NSError *error = nil;
103
+ return Reply(JSONBool([CanvistHostCalls selectAllWithId:editorId error:&error]), error);
104
+ }
105
+
106
+ - (NSString *)insertText:(NSString *)editorId text:(NSString *)text
107
+ {
108
+ NSError *error = nil;
109
+ return Reply(JSONBool([CanvistHostCalls insertTextWithId:editorId text:text error:&error]), error);
110
+ }
111
+
112
+ - (NSString *)undo:(NSString *)editorId
113
+ {
114
+ NSError *error = nil;
115
+ return Reply(JSONBool([CanvistHostCalls undoWithId:editorId error:&error]), error);
116
+ }
117
+
118
+ - (NSString *)redo:(NSString *)editorId
119
+ {
120
+ NSError *error = nil;
121
+ return Reply(JSONBool([CanvistHostCalls redoWithId:editorId error:&error]), error);
122
+ }
123
+
124
+ - (NSString *)canUndo:(NSString *)editorId
125
+ {
126
+ NSError *error = nil;
127
+ return Reply(JSONBool([CanvistHostCalls canUndoWithId:editorId error:&error]), error);
128
+ }
129
+
130
+ - (NSString *)canRedo:(NSString *)editorId
131
+ {
132
+ NSError *error = nil;
133
+ return Reply(JSONBool([CanvistHostCalls canRedoWithId:editorId error:&error]), error);
134
+ }
135
+
136
+ @end
@@ -0,0 +1,3 @@
1
+ // The Objective-C face of the `Canvist` pod's Swift: `CanvistHostView` and
2
+ // `CanvistHostCalls`. The podspec puts its directory on the search path.
3
+ #import "Canvist-Swift.h"
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
- "name": "@canvist/lynx",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@canvist/lynx",
3
+ "version": "0.2.0",
4
+ "description": "The canvist rich-text editor for Lynx: the native iOS and Android editors as a Lynx element, with commands and queries typed from the extensions you register.",
5
+ "license": "Unlicense",
6
+ "homepage": "https://github.com/canvist/canvist/tree/main/packages/canvist-lynx#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/canvist/canvist.git",
10
+ "directory": "packages/canvist-lynx"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/canvist/canvist/issues",
14
+ "email": "support@canvist.dev"
15
+ },
16
+ "keywords": ["lynx", "reactlynx", "rich-text", "editor", "ios", "android"],
17
+ "type": "module",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/mod.d.ts",
21
+ "default": "./dist/mod.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "ios",
27
+ "android/build.gradle",
28
+ "android/src/main",
29
+ "CanvistLynx.podspec"
30
+ ],
31
+ "sideEffects": false,
32
+ "scripts": {
33
+ "check:types": "tsc --noEmit",
34
+ "test": "rstest run",
35
+ "build": "tsc -p tsconfig.build.json",
36
+ "prepack": "npm run build"
37
+ },
38
+ "dependencies": {
39
+ "@canvist/headless": "^0.2.0"
40
+ },
41
+ "peerDependencies": {
42
+ "@lynx-js/react": ">=0.120.0",
43
+ "@lynx-js/types": ">=4.1.0"
44
+ },
45
+ "devDependencies": {
46
+ "@lynx-js/react": "^0.127.0",
47
+ "@lynx-js/react-rsbuild-plugin": "^0.20.3",
48
+ "@lynx-js/types": "^4.3.0",
49
+ "@rstest/core": "^0.12.1",
50
+ "@testing-library/dom": "^10.4.1",
51
+ "@testing-library/jest-dom": "^7.0.0",
52
+ "@types/react": "^19.2.0",
53
+ "jsdom": "^27.4.0",
54
+ "typescript": "^7.0.2"
55
+ }
56
+ }
package/readme.md ADDED
@@ -0,0 +1,110 @@
1
+ <p align="center">
2
+ <a href="https://github.com/canvist/canvist">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/canvist/canvist/main/assets/brand/vector/lockup-dark.svg">
5
+ <img width="220" src="https://raw.githubusercontent.com/canvist/canvist/main/assets/brand/vector/lockup.svg" alt="canvist" />
6
+ </picture>
7
+ </a>
8
+ </p>
9
+
10
+ <p align="center">
11
+ <a href="https://github.com/canvist/canvist/blob/main/docs/src/quick-start/lynx.md"><strong>Quick start</strong></a> ·
12
+ <a href="https://github.com/canvist/canvist/blob/main/docs/src/readme.md"><strong>Docs</strong></a> ·
13
+ <a href="https://github.com/canvist/canvist/blob/main/docs/src/reference/contributing.md"><strong>Contributing</strong></a> ·
14
+ <a href="https://github.com/canvist/canvist"><strong>canvist</strong></a>
15
+ </p>
16
+
17
+ <p align="center">
18
+ <a href="https://github.com/canvist/canvist/actions/workflows/lynx.yml"><img src="https://github.com/canvist/canvist/actions/workflows/lynx.yml/badge.svg?branch=main" alt="lynx status" /></a>
19
+ <a href="https://unlicense.org"><img src="https://img.shields.io/badge/license-Unlicense-3B2CF5" alt="Unlicense" /></a>
20
+ </p>
21
+
22
+ # @canvist/lynx
23
+
24
+ canvist's rich-text editor for [Lynx](https://lynxjs.org): the native iOS and Android `CanvistView`s as a `<canvist-editor>` element, not a web view. Typing goes through each platform's own text input, and the Rust engine lays out and paints every line. Commands, queries, and documents are typed from the extensions you register, with the same API as [`@canvist/web`](../canvist-web) and [`@canvist/react-native`](../canvist-react-native).
25
+
26
+ ```tsx
27
+ import { CanvistEditor } from "@canvist/lynx";
28
+
29
+ <CanvistEditor
30
+ content={{ format: "markdown", input: "# Notes" }}
31
+ onUpdate={(editor) => save(editor.export("markdown"))}
32
+ accessibility-label="Note"
33
+ style={{ flex: 1 }}
34
+ />;
35
+ ```
36
+
37
+ Needs ReactLynx, Lynx 4.1, iOS 17, and Android 7 (API 24).
38
+
39
+ ## A toolbar that follows the selection
40
+
41
+ ```tsx
42
+ import {
43
+ bold,
44
+ CanvistView,
45
+ heading,
46
+ useCanvistEditor,
47
+ useEditorState,
48
+ } from "@canvist/lynx";
49
+
50
+ function Notes() {
51
+ const { editor, props } = useCanvistEditor({ extensions: [bold, heading] });
52
+ const isBold = useEditorState(editor, (e) => e.query.isBold(), false);
53
+
54
+ return (
55
+ <view style={{ flex: 1 }}>
56
+ {/* `ignore-focus` keeps the keyboard up while you tap the toolbar. */}
57
+ <view ignore-focus bindtap={() => editor?.commands.toggleBold()}>
58
+ <text>{isBold ? "Unbold" : "Bold"}</text>
59
+ </view>
60
+ <CanvistView {...props} style={{ flex: 1 }} />
61
+ </view>
62
+ );
63
+ }
64
+ ```
65
+
66
+ The editor has the same calls as on React Native: `commands`, `can`, `query`, `run`, `ask`, `setContent`, `export`, `toJson`, `selection`, `select`, `insertText`, `undo`, `redo`, `focus`, `blur`, and `subscribe`. They are synchronous, and failures throw a `CanvistError` with a stable `code`.
67
+
68
+ ## Install
69
+
70
+ The native editors aren't on a package registry yet, so build them from the repository, and register the element and its native module in your host app.
71
+
72
+ **iOS.** Build the engine with `platforms/apple/build.sh`, add both pods to your Podfile, and register the module on your `LynxConfig`. The element registers itself as `<canvist-editor>`:
73
+
74
+ ```ruby
75
+ pod "Canvist", path: "path/to/canvist/platforms/apple"
76
+ pod "CanvistLynx", path: "path/to/canvist/packages/canvist-lynx"
77
+ ```
78
+
79
+ ```swift
80
+ let config = LynxConfig(provider: provider)
81
+ config.register(CanvistLynxModule.self)
82
+ ```
83
+
84
+ **Android.** Publish `canvist-android` to Maven Local with `platforms/android/build.sh` and `./gradlew :canvist:publishToMavenLocal`, include `packages/canvist-lynx/android` as a Gradle project, and register both pieces:
85
+
86
+ ```kotlin
87
+ LynxEnv.inst().init(application, null, null, null)
88
+ CanvistLynx.registerModule()
89
+
90
+ val lynxView = LynxViewBuilder()
91
+ .setTemplateProvider(provider)
92
+ .addBehavior(CanvistLynx.behavior())
93
+ .build(context)
94
+ ```
95
+
96
+ Lynx knows neither the safe area nor the keyboard, so size your LynxView to fit between them, as [`examples/lynx`](../../examples/lynx) does.
97
+
98
+ <details>
99
+ <summary><strong>Development</strong></summary>
100
+
101
+ ```sh
102
+ npm test # rstest: the typed editor, the element's attributes and events
103
+ npx tsc --noEmit # types, including the type-level tests
104
+ ../../examples/lynx/e2e/test.sh ios # the example on a simulator, driven by Maestro
105
+ ../../examples/lynx/e2e/test.sh android
106
+ ```
107
+
108
+ The build keeps the JSX (`dist/*.jsx`) for the app's ReactLynx compiler, as Lynx libraries do. The native module answers with a JSON envelope rather than throwing, because Lynx reports native exceptions to the host rather than to JavaScript.
109
+
110
+ </details>
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.