braid-text 0.0.1
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 +122 -0
- package/client.js +173 -0
- package/editor.html +79 -0
- package/index.js +1263 -0
- package/markdown-editor.html +316 -0
- package/package.json +12 -0
- package/server-demo.js +79 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
<html lang="en">
|
|
2
|
+
<script type="statebus">
|
|
3
|
+
dom.BODY = -> DIV(WIKI())
|
|
4
|
+
</script>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=.62"/>
|
|
6
|
+
|
|
7
|
+
<script src="https://stateb.us/client6.js" server="none"></script>
|
|
8
|
+
<script src="https://invisible.college/js/marked.min.js"></script>
|
|
9
|
+
|
|
10
|
+
<script src="https://braid.org/code/myers-diff1.js"></script>
|
|
11
|
+
<script>
|
|
12
|
+
window.statebus_fetch = window.fetch
|
|
13
|
+
window.fetch = window.og_fetch
|
|
14
|
+
</script>
|
|
15
|
+
<script src="https://unpkg.com/braid-http@~0.3/braid-http-client.js"></script>
|
|
16
|
+
<script>
|
|
17
|
+
window.fetch = window.statebus_fetch
|
|
18
|
+
</script>
|
|
19
|
+
<script src="https://unpkg.com/braid-text/client.js"></script>
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
|
|
23
|
+
var apply_patches_and_update_selection, diff, first_time, i, j, render_delay, scroll, braid_text_key, t, timer, ting, toggle_editor, update_markdown, update_markdown_later;
|
|
24
|
+
|
|
25
|
+
braid_text_key = location.pathname;
|
|
26
|
+
|
|
27
|
+
t = function() {
|
|
28
|
+
return document.getElementById('the editor');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
var braid_text = braid_text_client(braid_text_key, {
|
|
32
|
+
apply_remote_update: function(x) {
|
|
33
|
+
if (x.state !== void 0) {
|
|
34
|
+
t().value = x.state;
|
|
35
|
+
} else {
|
|
36
|
+
apply_patches_and_update_selection(t(), x.patches);
|
|
37
|
+
}
|
|
38
|
+
state.source = t().value;
|
|
39
|
+
update_markdown_later();
|
|
40
|
+
return t().value;
|
|
41
|
+
},
|
|
42
|
+
generate_local_diff_update: function(prev_state) {
|
|
43
|
+
var patches;
|
|
44
|
+
patches = diff(prev_state, t().value);
|
|
45
|
+
if (patches.length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
patches: patches,
|
|
50
|
+
state: t().value
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
window.statebus_ready || (window.statebus_ready = []);
|
|
56
|
+
|
|
57
|
+
window.statebus_ready.push(function() {
|
|
58
|
+
state.vert = true;
|
|
59
|
+
state.editing = false;
|
|
60
|
+
state.source = '';
|
|
61
|
+
// Toggle the editor with keyboard or edit button
|
|
62
|
+
document.body.onkeydown = function(e) {
|
|
63
|
+
if (e.keyCode === 27) { // Escape key
|
|
64
|
+
e.stopPropagation();
|
|
65
|
+
toggle_editor();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
// Switch to vertical layout when you resize
|
|
69
|
+
window.onresize = function() {
|
|
70
|
+
return state.vert = window.innerWidth < 1200;
|
|
71
|
+
};
|
|
72
|
+
return onresize();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Diffing and Patching Utilities
|
|
76
|
+
diff = function(before, after) {
|
|
77
|
+
var d, diff2, j, len, offset, p, patches;
|
|
78
|
+
diff2 = diff_main(before, after);
|
|
79
|
+
// Now we just need to reformat the output from diff_main into some
|
|
80
|
+
// nice json objects
|
|
81
|
+
patches = [];
|
|
82
|
+
offset = 0;
|
|
83
|
+
for (j = 0, len = diff2.length; j < len; j++) {
|
|
84
|
+
d = diff2[j];
|
|
85
|
+
p = null;
|
|
86
|
+
if (d[0] === 1) {
|
|
87
|
+
p = {
|
|
88
|
+
range: [offset, offset],
|
|
89
|
+
content: d[1]
|
|
90
|
+
};
|
|
91
|
+
} else if (d[0] === -1) {
|
|
92
|
+
p = {
|
|
93
|
+
range: [offset, offset + d[1].length],
|
|
94
|
+
content: ''
|
|
95
|
+
};
|
|
96
|
+
offset += d[1].length;
|
|
97
|
+
} else {
|
|
98
|
+
offset += d[1].length;
|
|
99
|
+
}
|
|
100
|
+
if (p) {
|
|
101
|
+
patches.push(p);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return patches;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
apply_patches_and_update_selection = function(textarea, patches) {
|
|
108
|
+
var i, j, k, l, len, len1, m, offset, original, p, range, ref, ref1, sel;
|
|
109
|
+
// convert from absolute to relative coordinates
|
|
110
|
+
offset = 0;
|
|
111
|
+
for (j = 0, len = patches.length; j < len; j++) {
|
|
112
|
+
p = patches[j];
|
|
113
|
+
p.range[0] += offset;
|
|
114
|
+
p.range[1] += offset;
|
|
115
|
+
offset -= p.range[1] - p.range[0];
|
|
116
|
+
offset += p.content.length;
|
|
117
|
+
}
|
|
118
|
+
original = textarea.value;
|
|
119
|
+
sel = [
|
|
120
|
+
textarea.selectionStart,
|
|
121
|
+
textarea.selectionEnd // Current cursor & selection
|
|
122
|
+
];
|
|
123
|
+
for (k = 0, len1 = patches.length; k < len1; k++) {
|
|
124
|
+
p = patches[k];
|
|
125
|
+
range = p.range;
|
|
126
|
+
// Update the cursor locations
|
|
127
|
+
for (i = l = 0, ref = sel.length; (0 <= ref ? l < ref : l > ref); i = 0 <= ref ? ++l : --l) {
|
|
128
|
+
if (sel[i] > range[0]) {
|
|
129
|
+
if (sel[i] > range[1]) {
|
|
130
|
+
sel[i] -= range[1] - range[0];
|
|
131
|
+
} else {
|
|
132
|
+
sel[i] = range[0];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (i = m = 0, ref1 = sel.length; (0 <= ref1 ? m < ref1 : m > ref1); i = 0 <= ref1 ? ++m : --m) {
|
|
137
|
+
if (sel[i] > range[0]) {
|
|
138
|
+
sel[i] += p.content.length;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Update the text with the new value
|
|
142
|
+
original = original.substring(0, range[0]) + p.content + original.substring(range[1]);
|
|
143
|
+
}
|
|
144
|
+
textarea.value = original;
|
|
145
|
+
textarea.selectionStart = sel[0];
|
|
146
|
+
return textarea.selectionEnd = sel[1];
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Render everything
|
|
150
|
+
dom.WIKI = function() {
|
|
151
|
+
var o;
|
|
152
|
+
// output
|
|
153
|
+
return DIV({}, DIV({
|
|
154
|
+
className: 'pad',
|
|
155
|
+
maxWidth: 750,
|
|
156
|
+
width: state.editing && !state.vert ? '55vw' : void 0
|
|
157
|
+
}, (function() {
|
|
158
|
+
var j, len, ref, results;
|
|
159
|
+
ref = state.outputs || [];
|
|
160
|
+
results = [];
|
|
161
|
+
for (j = 0, len = ref.length; j < len; j++) {
|
|
162
|
+
o = ref[j];
|
|
163
|
+
results.push(DIV({
|
|
164
|
+
dangerouslySetInnerHTML: {
|
|
165
|
+
__html: o
|
|
166
|
+
}
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
return results;
|
|
170
|
+
// bottom pad
|
|
171
|
+
})()), DIV({
|
|
172
|
+
height: '50vh',
|
|
173
|
+
display: !state.editing || !state.vert ? 'none' : void 0
|
|
174
|
+
}), TEXTAREA({
|
|
175
|
+
position: 'fixed',
|
|
176
|
+
hyphens: 'none',
|
|
177
|
+
bottom: 0,
|
|
178
|
+
right: 0,
|
|
179
|
+
width: state.vert ? '100%' : '45vw',
|
|
180
|
+
height: state.vert ? '50vh' : '100%',
|
|
181
|
+
display: !state.editing ? 'none' : void 0,
|
|
182
|
+
fontSize: 15,
|
|
183
|
+
fontFamily: 'helvetica, arial, avenir, lucida grande',
|
|
184
|
+
id: 'the editor',
|
|
185
|
+
onChange: function(e) {
|
|
186
|
+
if (!e.target.value && e.target.value !== '') {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Bail on edits that try to wipe us out
|
|
190
|
+
state.source = e.target.value;
|
|
191
|
+
braid_text.changed();
|
|
192
|
+
return update_markdown_later();
|
|
193
|
+
},
|
|
194
|
+
defaultValue: state.source
|
|
195
|
+
}), DIV({
|
|
196
|
+
position: 'fixed',
|
|
197
|
+
bottom: 0,
|
|
198
|
+
right: 0,
|
|
199
|
+
padding: 30,
|
|
200
|
+
cursor: 'pointer',
|
|
201
|
+
textDecoration: 'none',
|
|
202
|
+
backgroundColor: 'rgba(250, 250, 250, .5)',
|
|
203
|
+
onClick: toggle_editor
|
|
204
|
+
}, 'edit'));
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Render markdown after a delay
|
|
208
|
+
timer = null;
|
|
209
|
+
|
|
210
|
+
render_delay = 100;
|
|
211
|
+
|
|
212
|
+
update_markdown_later = function() {
|
|
213
|
+
if (timer) {
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
}
|
|
216
|
+
return timer = setTimeout(update_markdown, render_delay);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
update_markdown = function() {
|
|
220
|
+
var e, i, j, len, parse_markdown, ref, results, s, sources;
|
|
221
|
+
parse_markdown = function() {
|
|
222
|
+
var match, matches;
|
|
223
|
+
matches = (function() {
|
|
224
|
+
var results;
|
|
225
|
+
results = [];
|
|
226
|
+
while (match = /\n\|{3,}([^\n]*)\n/g.exec(state.source)) {
|
|
227
|
+
results.push(match[1]);
|
|
228
|
+
}
|
|
229
|
+
return results;
|
|
230
|
+
})();
|
|
231
|
+
return matches;
|
|
232
|
+
};
|
|
233
|
+
try {
|
|
234
|
+
if (!state.source) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
sources = state.source.split(/\n\|{3,}[^\n]*\n/g);
|
|
238
|
+
timer = null;
|
|
239
|
+
if (!state.sources || sources.length !== state.sources.length) {
|
|
240
|
+
state.sources = sources.splice();
|
|
241
|
+
state.outputs = (function() {
|
|
242
|
+
var j, len, results;
|
|
243
|
+
results = [];
|
|
244
|
+
for (j = 0, len = sources.length; j < len; j++) {
|
|
245
|
+
s = sources[j];
|
|
246
|
+
results.push(marked(s, {
|
|
247
|
+
sanitize: false
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
return results;
|
|
251
|
+
})();
|
|
252
|
+
return document.body.className = 'nopad';
|
|
253
|
+
} else {
|
|
254
|
+
ref = state.sources;
|
|
255
|
+
// But most of the time we just redo one section
|
|
256
|
+
results = [];
|
|
257
|
+
for (i = j = 0, len = ref.length; j < len; i = ++j) {
|
|
258
|
+
s = ref[i];
|
|
259
|
+
if (s !== sources[i]) {
|
|
260
|
+
state.sources[i] = sources[i];
|
|
261
|
+
results.push(state.outputs[i] = marked(s, {
|
|
262
|
+
sanitize: false
|
|
263
|
+
}));
|
|
264
|
+
} else {
|
|
265
|
+
results.push(void 0);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return results;
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
e = error;
|
|
272
|
+
return console.error('parse failure with', e);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
update_markdown();
|
|
277
|
+
|
|
278
|
+
first_time = true;
|
|
279
|
+
|
|
280
|
+
toggle_editor = function() {
|
|
281
|
+
state.editing = !state.editing;
|
|
282
|
+
if (state.editing) {
|
|
283
|
+
t().focus();
|
|
284
|
+
}
|
|
285
|
+
if (state.editing && first_time) {
|
|
286
|
+
first_time = false;
|
|
287
|
+
t().setSelectionRange(0, 0);
|
|
288
|
+
t().scrollTop = 0;
|
|
289
|
+
}
|
|
290
|
+
return update_markdown();
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Support #hashtag scrolling into view
|
|
294
|
+
ting = null;
|
|
295
|
+
|
|
296
|
+
scroll = function() {
|
|
297
|
+
// We only scroll to the ting once -- if it's fresh
|
|
298
|
+
if (ting || location.hash.length === 0) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
ting = document.getElementById(location.hash.substr(1));
|
|
302
|
+
return ting && ting.scrollIntoView();
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
for (i = j = 0; j <= 50; i = ++j) {
|
|
306
|
+
setTimeout(scroll, i / 5.0 * 1000);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
</script>
|
|
310
|
+
|
|
311
|
+
<link rel="stylesheet" href="https://invisible.college/css/github-markdown.css">
|
|
312
|
+
<style>
|
|
313
|
+
body{-ms-hyphens: auto;-webkit-hyphens: auto;hyphens: auto;}
|
|
314
|
+
h1,h2,h3,h4 {text-align: left; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none;}
|
|
315
|
+
note {position: absolute; left: 720px; width: 270px; background-color: #F8F3B7; padding: 10px; box-shadow: -2px 2px 2px #ccc; border-radius: 2px; text-align: left;}
|
|
316
|
+
</style>
|
package/package.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "braid-text",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Library for collaborative text over http using braid.",
|
|
5
|
+
"author": "Braid Working Group",
|
|
6
|
+
"repository": "braid-org/braidjs",
|
|
7
|
+
"homepage": "https://braid.org",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"diamond-types-node": "^1.0.2",
|
|
10
|
+
"braid-http": "^0.3.18"
|
|
11
|
+
}
|
|
12
|
+
}
|
package/server-demo.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
|
|
2
|
+
var port = 8888
|
|
3
|
+
|
|
4
|
+
var braid_text = require("./index.js")
|
|
5
|
+
|
|
6
|
+
// TODO: set a custom database folder
|
|
7
|
+
// (the default is ./braid-text-db)
|
|
8
|
+
//
|
|
9
|
+
// braid_text.db_folder = './custom_db_folder'
|
|
10
|
+
|
|
11
|
+
var server = require("http").createServer(async (req, res) => {
|
|
12
|
+
console.log(`${req.method} ${req.url}`)
|
|
13
|
+
|
|
14
|
+
if (req.url.endsWith("?editor")) {
|
|
15
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-cache" })
|
|
16
|
+
require("fs").createReadStream("./editor.html").pipe(res)
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (req.url.endsWith("?markdown-editor")) {
|
|
21
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-cache" })
|
|
22
|
+
require("fs").createReadStream("./markdown-editor.html").pipe(res)
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// TODO: uncomment out the code below to add /pages endpoint,
|
|
27
|
+
// which displays all the currently used keys
|
|
28
|
+
//
|
|
29
|
+
// if (req.url === '/pages') {
|
|
30
|
+
// var pages = new Set()
|
|
31
|
+
// for (let x of await require('fs').promises.readdir(db_folder)) {
|
|
32
|
+
// let m = x.match(/^(.*)\.\d+$/)
|
|
33
|
+
// if (m) pages.add(decodeURIComponent(m[1]))
|
|
34
|
+
// }
|
|
35
|
+
// res.writeHead(200, {
|
|
36
|
+
// "Content-Type": "application/json",
|
|
37
|
+
// "Access-Control-Allow-Origin": "*",
|
|
38
|
+
// "Access-Control-Allow-Methods": "*",
|
|
39
|
+
// "Access-Control-Allow-Headers": "*",
|
|
40
|
+
// "Access-Control-Expose-Headers": "*"
|
|
41
|
+
// })
|
|
42
|
+
// res.end(JSON.stringify([...pages.keys()]))
|
|
43
|
+
// return
|
|
44
|
+
// }
|
|
45
|
+
|
|
46
|
+
// TODO: uncomment and change admin_pass above,
|
|
47
|
+
// and uncomment out the code below to add basic access control
|
|
48
|
+
//
|
|
49
|
+
// var admin_pass = "fake_password"
|
|
50
|
+
//
|
|
51
|
+
// if (req.url === '/login_' + admin_pass) {
|
|
52
|
+
// res.writeHead(200, {
|
|
53
|
+
// "Content-Type": "text/plain",
|
|
54
|
+
// "Set-Cookie": `admin_pass=${admin_pass}; Path=/`,
|
|
55
|
+
// });
|
|
56
|
+
// res.end("Logged in successfully");
|
|
57
|
+
// return;
|
|
58
|
+
// }
|
|
59
|
+
//
|
|
60
|
+
// if (req.method == "PUT" || req.method == "POST" || req.method == "PATCH") {
|
|
61
|
+
// if (!req.headers.cookie?.includes(`admin_pass=${admin_pass}`)) {
|
|
62
|
+
// console.log("Blocked PUT:", { cookie: req.headers.cookie })
|
|
63
|
+
// res.statusCode = 401
|
|
64
|
+
// return res.end()
|
|
65
|
+
// }
|
|
66
|
+
// }
|
|
67
|
+
|
|
68
|
+
// Create some initial text for new documents
|
|
69
|
+
if (await braid_text.get(req.url) === undefined) {
|
|
70
|
+
await braid_text.put(req.url, {body: 'This is a fresh blank document, ready for you to edit.' })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Now serve the collaborative text!
|
|
74
|
+
braid_text.serve(req, res)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
server.listen(port, () => {
|
|
78
|
+
console.log(`server started on port ${port}`)
|
|
79
|
+
})
|