braid-text 0.2.47 → 0.2.49
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/README.md +72 -50
- package/editor.html +3 -10
- package/markdown-editor.html +5 -18
- package/package.json +1 -1
- package/simpleton-client.js +94 -21
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Or try opening the URL in [Braid-Chrome](https://github.com/braid-org/braid-chro
|
|
|
37
37
|
|
|
38
38
|
Check out the `server-demo.js` file to see examples for how to add simple access control, where a user need only enter a password into a cookie in the javascript console like: `document.cookie = 'password'`; and a `/pages` endpoint to show all the edited pages.
|
|
39
39
|
|
|
40
|
-
## General Use
|
|
40
|
+
## General Use as Server
|
|
41
41
|
|
|
42
42
|
Install it in your project:
|
|
43
43
|
```shell
|
|
@@ -94,51 +94,52 @@ http_server.on("request", (req, res) => {
|
|
|
94
94
|
- `patches`: <small style="color:lightgrey">[optional]</small> Array of patches, each of the form `{unit: 'text', range: '[1:3]', content: 'hi'}`, which would replace the second and third unicode code-points in the text with `hi`. See Braid [Range-Patches](https://github.com/braid-org/braid-spec/blob/master/draft-toomim-httpbis-range-patch-01.txt).
|
|
95
95
|
- `peer`: <small style="color:lightgrey">[optional]</small> Identifies this peer. This mutation will not be echoed back to `get` subscriptions that use this same `peer` header.
|
|
96
96
|
|
|
97
|
-
## General Use
|
|
97
|
+
## General Use as Client
|
|
98
|
+
|
|
99
|
+
Here's a basic running example to start:
|
|
98
100
|
|
|
99
101
|
```html
|
|
102
|
+
<!-- 1. Your textarea -->
|
|
103
|
+
<textarea id="my_textarea"></textarea>
|
|
104
|
+
|
|
105
|
+
<!-- 2. Include the library -->
|
|
106
|
+
<script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
|
|
100
107
|
<script src="https://unpkg.com/braid-text/simpleton-client.js"></script>
|
|
101
|
-
<script>
|
|
102
108
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// Example data:
|
|
110
|
-
// state: "Hello World" // The new text
|
|
111
|
-
// patches: [{ range: [5, 5], content: " World" }] // Patches that create the new text
|
|
112
|
-
//
|
|
113
|
-
// Then return the new state of textarea as a string:
|
|
114
|
-
return new_state
|
|
115
|
-
},
|
|
116
|
-
generate_local_diff_update: (prev_state) => {
|
|
117
|
-
|
|
118
|
-
// Compute diff between prev_state ^ and the current textarea string, such as:
|
|
119
|
-
//
|
|
120
|
-
// var patches = [{
|
|
121
|
-
// range: [5, 5], // The range from position 5 to position 5
|
|
122
|
-
// content: " World" // is replaced with the string " World"
|
|
123
|
-
// }]
|
|
124
|
-
//
|
|
125
|
-
// ...to insert something after a prev_state of "Hello".
|
|
126
|
-
|
|
127
|
-
// Then return the new state (as a string) and the diff (as `patches`)
|
|
128
|
-
return {new_state, patches}
|
|
129
|
-
},
|
|
109
|
+
<!-- 3. Wire it up -->
|
|
110
|
+
<script>
|
|
111
|
+
// Connect to server
|
|
112
|
+
var simpleton = simpleton_client('https://braid.org/simpleton_example', {
|
|
113
|
+
on_state: state => my_textarea.value = state, // incoming changes
|
|
114
|
+
get_state: () => my_textarea.value // outgoing changes
|
|
130
115
|
})
|
|
131
|
-
|
|
132
|
-
...
|
|
133
|
-
|
|
134
|
-
// When changes occur in client's textarea, let simpleton know,
|
|
135
|
-
// so that it can call generate_local_diff_update() to ask for them.
|
|
136
|
-
simpleton.changed()
|
|
137
116
|
|
|
117
|
+
// Tell simpleton when user types
|
|
118
|
+
my_textarea.oninput = () => simpleton.changed()
|
|
138
119
|
</script>
|
|
139
120
|
```
|
|
140
121
|
|
|
141
|
-
|
|
122
|
+
You should see something if you run this (though the server in that example will likely ignore your changes).
|
|
123
|
+
|
|
124
|
+
The basic idea is that you create a simpleton_client and tell it to connect to a url. Any incoming changes trigger the `on_state` callback. For outgoing changes, you'd think we would tell simpleton the new state when we get the `oninput` event — however, the network may be backed up, and simpleton wants to hold off on sending more updates. So instead, we just inform simpleton that a change has occurred and let it handle when to actually send the change to the server.
|
|
125
|
+
|
|
126
|
+
This decoupling between `simpleton.changed` and `get_state` is especially helpful when disconnecting from the server for extended periods (like getting on a plane for a couple hours). Without it, simpleton would have queued up a message for every change made during those two hours and try to send them all upon reconnection. With the decoupling, simpleton can wait until reconnection and then request a single diff to the new final state.
|
|
127
|
+
|
|
128
|
+
### Finer-Grained Integration
|
|
129
|
+
|
|
130
|
+
There are a couple ways to do a more fine-grained integration:
|
|
131
|
+
|
|
132
|
+
**1. Receiving patches instead of full state**
|
|
133
|
+
|
|
134
|
+
Rather than receiving a completely new state in `on_state`, you can receive just changes to the current state in `on_patches`, as an array of patches. This can be more efficient, especially if you're integrating with an editor that has an API for making edits (rather than needing to set the whole string, as with an HTML textarea). Even with a textarea though, getting patches can be helpful for updating the cursor or selection position.
|
|
135
|
+
|
|
136
|
+
**2. Supplying custom patch generation**
|
|
137
|
+
|
|
138
|
+
Rather than only supplying `get_state`, you can also supply `get_patches` — a function that accepts a string representing the document at some point in the past and returns an array of patches to get from there to the current state.
|
|
139
|
+
|
|
140
|
+
Simpleton has a default simple way of computing this (scanning for a common prefix and suffix), but you may have a better function that handles complex cases. For example, when a user pastes in what seems like a large change that replaces the entire document, proper diff analysis might reveal it's just a few edits here and there. Also, with some editors, the analogous `oninput` event may provide patches for free, making `get_patches` more efficient.
|
|
141
|
+
|
|
142
|
+
You can see an example of both these finer-grained integration techniques here: [editor.html](https://raw.githubusercontent.com/braid-org/braid-text/master/editor.html).
|
|
142
143
|
|
|
143
144
|
## Client API
|
|
144
145
|
|
|
@@ -148,38 +149,59 @@ simpleton = simpleton_client(url, options)
|
|
|
148
149
|
|
|
149
150
|
- `url`: The URL of the resource to synchronize with.
|
|
150
151
|
- `options`: An object containing the following properties:
|
|
151
|
-
|
|
152
|
+
|
|
153
|
+
### Incoming Updates
|
|
154
|
+
|
|
155
|
+
- `on_patches`: <small style="color:lightgrey">[optional]</small> A function called when patches are received from the server:
|
|
152
156
|
|
|
153
157
|
```javascript
|
|
154
|
-
(
|
|
158
|
+
(patches) => {...}
|
|
155
159
|
```
|
|
156
160
|
|
|
157
|
-
- `
|
|
158
|
-
- `patches`: If present, an array of patch objects, each representing a string-replace operation. Each patch object has the following properties:
|
|
161
|
+
- `patches`: An array of patch objects, each representing a string-replace operation. Each patch object has:
|
|
159
162
|
- `range`: An array of two numbers, `[start, end]`, specifying the start and end positions of the characters to be deleted.
|
|
160
163
|
- `content`: The text to be inserted in place of the deleted characters.
|
|
161
164
|
|
|
162
165
|
Note that patches will always be in order, but the range positions of each patch reference the original string, i.e., the second patch's range values do not take into account the application of the first patch.
|
|
163
166
|
|
|
164
|
-
|
|
167
|
+
- `on_state`: <small style="color:lightgrey">[optional]</small> A function called when a complete state update is received from the server:
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
(state) => {...}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- `state`: The new complete value of the text.
|
|
174
|
+
|
|
175
|
+
### Local State Management
|
|
165
176
|
|
|
166
|
-
- `
|
|
177
|
+
- `get_state`: **[required]** A function that returns the current state of the text:
|
|
167
178
|
|
|
168
179
|
```javascript
|
|
169
|
-
(
|
|
180
|
+
() => current_state
|
|
170
181
|
```
|
|
171
182
|
|
|
172
|
-
|
|
183
|
+
- `get_patches`: <small style="color:lightgrey">[optional]</small> A function that generates patches representing changes between a previous state and the current state:
|
|
173
184
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
185
|
+
```javascript
|
|
186
|
+
(prev_state) => patches
|
|
187
|
+
```
|
|
177
188
|
|
|
178
|
-
|
|
189
|
+
Returns an array of patch objects representing the changes. The default implementation finds a common prefix and suffix for a simple diff, but you can provide a more sophisticated implementation or track patches directly from your editor.
|
|
190
|
+
|
|
191
|
+
### Other Options
|
|
179
192
|
|
|
180
193
|
- `content_type`: <small style="color:lightgrey">[optional]</small> If set, this value will be sent in the `Accept` and `Content-Type` headers to the server.
|
|
181
194
|
|
|
182
|
-
|
|
195
|
+
### Methods
|
|
196
|
+
|
|
197
|
+
- `simpleton.changed()`: Call this function to report local updates whenever they occur, e.g., in the `oninput` event handler of a textarea being synchronized. The system will call `get_patches` when it needs to send updates to the server.
|
|
198
|
+
|
|
199
|
+
### Deprecated Options
|
|
200
|
+
|
|
201
|
+
The following options are deprecated and should be replaced with the new API:
|
|
202
|
+
|
|
203
|
+
- ~~`apply_remote_update`~~ → Use `on_patches` and `on_state` instead
|
|
204
|
+
- ~~`generate_local_diff_update`~~ → Use `get_patches` and `get_state` instead
|
|
183
205
|
|
|
184
206
|
## Testing
|
|
185
207
|
|
package/editor.html
CHANGED
|
@@ -9,16 +9,9 @@
|
|
|
9
9
|
<script src="/simpleton-client.js"></script>
|
|
10
10
|
<script>
|
|
11
11
|
let simpleton = simpleton_client(location.pathname, {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return texty.value;
|
|
16
|
-
},
|
|
17
|
-
generate_local_diff_update: (prev_state) => {
|
|
18
|
-
var patches = diff(prev_state, texty.value);
|
|
19
|
-
if (patches.length === 0) return null;
|
|
20
|
-
return { patches, new_state: texty.value };
|
|
21
|
-
},
|
|
12
|
+
on_patches: (patches) => apply_patches_and_update_selection(texty, patches),
|
|
13
|
+
get_patches: (prev_state) => diff(prev_state, texty.value),
|
|
14
|
+
get_state: () => texty.value,
|
|
22
15
|
on_error: (e) => {
|
|
23
16
|
texty.disabled = true
|
|
24
17
|
texty.style.background = '#fee'
|
package/markdown-editor.html
CHANGED
|
@@ -27,27 +27,14 @@ t = function() {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
var simpleton = simpleton_client(location.pathname, {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
t().value = x.state;
|
|
33
|
-
} else {
|
|
34
|
-
apply_patches_and_update_selection(t(), x.patches);
|
|
35
|
-
}
|
|
30
|
+
on_patches: (patches) => {
|
|
31
|
+
apply_patches_and_update_selection(t(), patches);
|
|
36
32
|
state.source = t().value;
|
|
37
33
|
update_markdown_later();
|
|
38
|
-
return t().value;
|
|
39
|
-
},
|
|
40
|
-
generate_local_diff_update: function(prev_state) {
|
|
41
|
-
var patches;
|
|
42
|
-
patches = diff(prev_state, t().value);
|
|
43
|
-
if (patches.length === 0) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
patches: patches,
|
|
48
|
-
new_state: t().value
|
|
49
|
-
};
|
|
50
34
|
},
|
|
35
|
+
get_patches: (prev_state) => diff(prev_state, t().value),
|
|
36
|
+
get_state: () => t().value,
|
|
37
|
+
|
|
51
38
|
on_error: (e) => {
|
|
52
39
|
t().disabled = true
|
|
53
40
|
t().style.background = '#fee'
|
package/package.json
CHANGED
package/simpleton-client.js
CHANGED
|
@@ -2,22 +2,51 @@
|
|
|
2
2
|
//
|
|
3
3
|
// url: resource endpoint
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// on_patches?: (patches) => void
|
|
6
|
+
// processes incoming patches
|
|
7
|
+
//
|
|
8
|
+
// on_state?: (state) => void
|
|
9
|
+
// processes incoming state
|
|
10
|
+
//
|
|
11
|
+
// get_patches?: (prev_state) => patches
|
|
12
|
+
// returns patches representing diff
|
|
13
|
+
// between prev_state and current state,
|
|
14
|
+
// which are guaranteed to be different
|
|
15
|
+
// if this method is being called
|
|
16
|
+
// (the default does this in a fast/simple way,
|
|
17
|
+
// finding a common prefix and suffix,
|
|
18
|
+
// but you can supply something better,
|
|
19
|
+
// or possibly keep track of patches as they come from your editor)
|
|
20
|
+
//
|
|
21
|
+
// get_state: () => current_state
|
|
22
|
+
// returns the current state
|
|
23
|
+
//
|
|
24
|
+
// [DEPRECATED] apply_remote_update: ({patches, state}) => {...}
|
|
6
25
|
// this is for incoming changes;
|
|
7
26
|
// one of these will be non-null,
|
|
8
27
|
// and can be applied to the current state.
|
|
9
28
|
//
|
|
10
|
-
// generate_local_diff_update: (prev_state) => {...}
|
|
29
|
+
// [DEPRECATED] generate_local_diff_update: (prev_state) => {...}
|
|
11
30
|
// this is to generate outgoing changes,
|
|
12
31
|
// and if there are changes, returns { patches, new_state }
|
|
13
32
|
//
|
|
14
33
|
// content_type: used for Accept and Content-Type headers
|
|
15
34
|
//
|
|
16
|
-
// returns { changed
|
|
17
|
-
//
|
|
18
|
-
//
|
|
35
|
+
// returns { changed }
|
|
36
|
+
// call changed whenever there is a local change,
|
|
37
|
+
// and the system will call get_patches when it needs to.
|
|
19
38
|
//
|
|
20
|
-
function simpleton_client(url, {
|
|
39
|
+
function simpleton_client(url, {
|
|
40
|
+
on_patches,
|
|
41
|
+
on_state,
|
|
42
|
+
get_patches,
|
|
43
|
+
get_state,
|
|
44
|
+
apply_remote_update, // DEPRECATED
|
|
45
|
+
generate_local_diff_update, // DEPRECATED
|
|
46
|
+
content_type,
|
|
47
|
+
on_error,
|
|
48
|
+
on_res
|
|
49
|
+
}) {
|
|
21
50
|
var peer = Math.random().toString(36).substr(2)
|
|
22
51
|
var current_version = []
|
|
23
52
|
var prev_state = ""
|
|
@@ -69,7 +98,19 @@ function simpleton_client(url, { apply_remote_update, generate_local_diff_update
|
|
|
69
98
|
}
|
|
70
99
|
}
|
|
71
100
|
|
|
72
|
-
|
|
101
|
+
if (apply_remote_update) {
|
|
102
|
+
// DEPRECATED
|
|
103
|
+
prev_state = apply_remote_update(update)
|
|
104
|
+
} else {
|
|
105
|
+
var patches = update.patches ||
|
|
106
|
+
[{range: [0, 0], content: update.state}]
|
|
107
|
+
if (on_patches) {
|
|
108
|
+
on_patches(patches)
|
|
109
|
+
prev_state = get_state()
|
|
110
|
+
} else prev_state = apply_patches(prev_state, patches)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (on_state) on_state(prev_state)
|
|
73
114
|
}
|
|
74
115
|
}, on_error)
|
|
75
116
|
}).catch(on_error)
|
|
@@ -81,9 +122,17 @@ function simpleton_client(url, { apply_remote_update, generate_local_diff_update
|
|
|
81
122
|
changed: async () => {
|
|
82
123
|
if (outstanding_changes >= max_outstanding_changes) return
|
|
83
124
|
while (true) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
125
|
+
if (generate_local_diff_update) {
|
|
126
|
+
// DEPRECATED
|
|
127
|
+
var update = generate_local_diff_update(prev_state)
|
|
128
|
+
if (!update) return // Stop if there wasn't a change!
|
|
129
|
+
var {patches, new_state} = update
|
|
130
|
+
} else {
|
|
131
|
+
var new_state = get_state()
|
|
132
|
+
if (new_state === prev_state) return // Stop if there wasn't a change!
|
|
133
|
+
var patches = get_patches ? get_patches(prev_state) :
|
|
134
|
+
[simple_diff(prev_state, new_state)]
|
|
135
|
+
}
|
|
87
136
|
|
|
88
137
|
// convert from js-indicies to code-points
|
|
89
138
|
let c = 0
|
|
@@ -133,18 +182,42 @@ function simpleton_client(url, { apply_remote_update, generate_local_diff_update
|
|
|
133
182
|
}
|
|
134
183
|
}
|
|
135
184
|
}
|
|
136
|
-
}
|
|
137
185
|
|
|
138
|
-
function get_char_size(s, i) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
186
|
+
function get_char_size(s, i) {
|
|
187
|
+
const charCode = s.charCodeAt(i)
|
|
188
|
+
return (charCode >= 0xd800 && charCode <= 0xdbff) ? 2 : 1
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function count_code_points(str) {
|
|
192
|
+
let code_points = 0
|
|
193
|
+
for (let i = 0; i < str.length; i++) {
|
|
194
|
+
if (str.charCodeAt(i) >= 0xd800 && str.charCodeAt(i) <= 0xdbff) i++
|
|
195
|
+
code_points++
|
|
196
|
+
}
|
|
197
|
+
return code_points
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function simple_diff(a, b) {
|
|
201
|
+
// Find common prefix
|
|
202
|
+
var p = 0
|
|
203
|
+
var len = Math.min(a.length, b.length)
|
|
204
|
+
while (p < len && a[p] === b[p]) p++
|
|
142
205
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
206
|
+
// Find common suffix (from what remains after prefix)
|
|
207
|
+
var s = 0
|
|
208
|
+
len -= p
|
|
209
|
+
while (s < len && a[a.length - s - 1] === b[b.length - s - 1]) s++
|
|
210
|
+
|
|
211
|
+
return {range: [p, a.length - s], content: b.slice(p, b.length - s)}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function apply_patches(state, patches) {
|
|
215
|
+
var offset = 0
|
|
216
|
+
for (var p of patches) {
|
|
217
|
+
state = state.substring(0, p.range[0] + offset) + p.content +
|
|
218
|
+
state.substring(p.range[1] + offset)
|
|
219
|
+
offset += p.content.length - (p.range[1] - p.range[0])
|
|
220
|
+
}
|
|
221
|
+
return state
|
|
148
222
|
}
|
|
149
|
-
return code_points
|
|
150
223
|
}
|