@canvist/lynx 0.0.0 → 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.
- package/CanvistLynx.podspec +31 -0
- package/android/build.gradle +35 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/io/github/ifiokjr/canvist/lynx/CanvistLynx.java +33 -0
- package/android/src/main/java/io/github/ifiokjr/canvist/lynx/CanvistLynxEditor.java +142 -0
- package/android/src/main/kotlin/io/github/ifiokjr/canvist/lynx/CanvistLynxModule.kt +109 -0
- package/dist/bridge.d.ts +15 -0
- package/dist/bridge.js +64 -0
- package/dist/editor.d.ts +88 -0
- package/dist/editor.jsx +145 -0
- package/dist/mod.d.ts +28 -0
- package/dist/mod.js +3 -0
- package/dist/native.d.ts +80 -0
- package/dist/native.js +1 -0
- package/ios/CanvistLynxEditor.h +12 -0
- package/ios/CanvistLynxEditor.m +109 -0
- package/ios/CanvistLynxModule.h +14 -0
- package/ios/CanvistLynxModule.m +136 -0
- package/ios/CanvistSwift.h +3 -0
- package/package.json +56 -1
- package/readme.md +110 -0
- package/CHANGELOG.md +0 -3
- package/LICENSE +0 -24
- package/README.md +0 -3
|
@@ -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"
|
package/dist/bridge.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/editor.d.ts
ADDED
|
@@ -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;
|
package/dist/editor.jsx
ADDED
|
@@ -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
package/dist/native.d.ts
ADDED
|
@@ -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
|
package/package.json
CHANGED
|
@@ -1 +1,56 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
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/CHANGELOG.md
DELETED
package/LICENSE
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
This is free and unencumbered software released into the public domain.
|
|
2
|
-
|
|
3
|
-
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
-
distribute this software, either in source code form or as a compiled
|
|
5
|
-
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
-
means.
|
|
7
|
-
|
|
8
|
-
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
-
of this software dedicate any and all copyright interest in the
|
|
10
|
-
software to the public domain. We make this dedication for the benefit
|
|
11
|
-
of the public at large and to the detriment of our heirs and
|
|
12
|
-
successors. We intend this dedication to be an overt act of
|
|
13
|
-
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
-
software under copyright law.
|
|
15
|
-
|
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
-
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
-
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
-
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
-
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
-
|
|
24
|
-
For more information, please refer to <http://unlicense.org/>
|
package/README.md
DELETED