@roeehrl/tinode-sdk 0.25.1-sqlite.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/LICENSE +201 -0
- package/README.md +47 -0
- package/package.json +76 -0
- package/src/access-mode.js +567 -0
- package/src/cbuffer.js +244 -0
- package/src/cbuffer.test.js +107 -0
- package/src/comm-error.js +14 -0
- package/src/config.js +71 -0
- package/src/connection.js +537 -0
- package/src/db.js +1021 -0
- package/src/drafty.js +2758 -0
- package/src/drafty.test.js +1600 -0
- package/src/fnd-topic.js +123 -0
- package/src/index.js +29 -0
- package/src/index.native.js +35 -0
- package/src/large-file.js +325 -0
- package/src/me-topic.js +480 -0
- package/src/meta-builder.js +283 -0
- package/src/storage-sqlite.js +1081 -0
- package/src/tinode.js +2382 -0
- package/src/topic.js +2160 -0
- package/src/utils.js +309 -0
- package/src/utils.test.js +456 -0
- package/types/index.d.ts +1227 -0
- package/umd/tinode.dev.js +6856 -0
- package/umd/tinode.dev.js.map +1 -0
- package/umd/tinode.prod.js +2 -0
- package/umd/tinode.prod.js.map +1 -0
package/src/drafty.js
ADDED
|
@@ -0,0 +1,2758 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2015-2024 Tinode LLC.
|
|
3
|
+
* @summary Minimally rich text representation and formatting for Tinode.
|
|
4
|
+
* @license Apache 2.0
|
|
5
|
+
*
|
|
6
|
+
* @file Basic parser and formatter for very simple text markup. Mostly targeted at
|
|
7
|
+
* mobile use cases similar to Telegram, WhatsApp, and FB Messenger.
|
|
8
|
+
*
|
|
9
|
+
* <p>Supports conversion of user keyboard input to formatted text:</p>
|
|
10
|
+
* <ul>
|
|
11
|
+
* <li>*abc* → <b>abc</b></li>
|
|
12
|
+
* <li>_abc_ → <i>abc</i></li>
|
|
13
|
+
* <li>~abc~ → <del>abc</del></li>
|
|
14
|
+
* <li>`abc` → <tt>abc</tt></li>
|
|
15
|
+
* </ul>
|
|
16
|
+
* Also supports forms and buttons.
|
|
17
|
+
*
|
|
18
|
+
* Nested formatting is supported, e.g. *abc _def_* -> <b>abc <i>def</i></b>
|
|
19
|
+
* URLs, @mentions, and #hashtags are extracted and converted into links.
|
|
20
|
+
* Forms and buttons can be added procedurally.
|
|
21
|
+
* JSON data representation is inspired by Draft.js raw formatting.
|
|
22
|
+
*
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* Text:
|
|
26
|
+
* <pre>
|
|
27
|
+
* this is *bold*, `code` and _italic_, ~strike~
|
|
28
|
+
* combined *bold and _italic_*
|
|
29
|
+
* an url: https://www.example.com/abc#fragment and another _www.tinode.co_
|
|
30
|
+
* this is a @mention and a #hashtag in a string
|
|
31
|
+
* second #hashtag
|
|
32
|
+
* </pre>
|
|
33
|
+
*
|
|
34
|
+
* Sample JSON representation of the text above:
|
|
35
|
+
* {
|
|
36
|
+
* "txt": "this is bold, code and italic, strike combined bold and italic an url: https://www.example.com/abc#fragment " +
|
|
37
|
+
* "and another www.tinode.co this is a @mention and a #hashtag in a string second #hashtag",
|
|
38
|
+
* "fmt": [
|
|
39
|
+
* { "at":8, "len":4,"tp":"ST" },{ "at":14, "len":4, "tp":"CO" },{ "at":23, "len":6, "tp":"EM"},
|
|
40
|
+
* { "at":31, "len":6, "tp":"DL" },{ "tp":"BR", "len":1, "at":37 },{ "at":56, "len":6, "tp":"EM" },
|
|
41
|
+
* { "at":47, "len":15, "tp":"ST" },{ "tp":"BR", "len":1, "at":62 },{ "at":120, "len":13, "tp":"EM" },
|
|
42
|
+
* { "at":71, "len":36, "key":0 },{ "at":120, "len":13, "key":1 },{ "tp":"BR", "len":1, "at":133 },
|
|
43
|
+
* { "at":144, "len":8, "key":2 },{ "at":159, "len":8, "key":3 },{ "tp":"BR", "len":1, "at":179 },
|
|
44
|
+
* { "at":187, "len":8, "key":3 },{ "tp":"BR", "len":1, "at":195 }
|
|
45
|
+
* ],
|
|
46
|
+
* "ent": [
|
|
47
|
+
* { "tp":"LN", "data":{ "url":"https://www.example.com/abc#fragment" } },
|
|
48
|
+
* { "tp":"LN", "data":{ "url":"http://www.tinode.co" } },
|
|
49
|
+
* { "tp":"MN", "data":{ "val":"mention" } },
|
|
50
|
+
* { "tp":"HT", "data":{ "val":"hashtag" } }
|
|
51
|
+
* ]
|
|
52
|
+
* }
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
'use strict';
|
|
56
|
+
|
|
57
|
+
// NOTE TO DEVELOPERS:
|
|
58
|
+
// Localizable strings should be double quoted "строка на другом языке",
|
|
59
|
+
// non-localizable strings should be single quoted 'non-localized'.
|
|
60
|
+
|
|
61
|
+
const MAX_FORM_ELEMENTS = 8;
|
|
62
|
+
const MAX_PREVIEW_ATTACHMENTS = 3;
|
|
63
|
+
const MAX_PREVIEW_DATA_SIZE = 64;
|
|
64
|
+
const DRAFTY_MIME_TYPE = 'text/x-drafty';
|
|
65
|
+
// Drafty form-response MIME type.
|
|
66
|
+
const DRAFTY_FR_MIME_TYPE = 'text/x-drafty-fr';
|
|
67
|
+
// Legacy Drafty form-response MIME type.
|
|
68
|
+
const DRAFTY_FR_MIME_TYPE_LEGACY = 'application/json'; // Remove in 2026.
|
|
69
|
+
const ALLOWED_ENT_FIELDS = ['act', 'height', 'duration', 'incoming', 'mime', 'name', 'premime', 'preref', 'preview',
|
|
70
|
+
'ref', 'size', 'state', 'url', 'val', 'width'
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// Intl.Segmenter is not available in Firefox 124 and earlier. FF 125 with support for Intl.Segmenter
|
|
74
|
+
// was released on April 15, 2024. Polyfill is included in the top package (webapp).
|
|
75
|
+
const segmenter = new Intl.Segmenter();
|
|
76
|
+
|
|
77
|
+
// Regular expressions for parsing inline formats. Javascript does not support lookbehind,
|
|
78
|
+
// so it's a bit messy.
|
|
79
|
+
const INLINE_STYLES = [
|
|
80
|
+
// Strong = bold, *bold text*
|
|
81
|
+
{
|
|
82
|
+
name: 'ST',
|
|
83
|
+
start: /(?:^|[\W_])(\*)[^\s*]/,
|
|
84
|
+
end: /[^\s*](\*)(?=$|[\W_])/
|
|
85
|
+
},
|
|
86
|
+
// Emphesized = italic, _italic text_
|
|
87
|
+
{
|
|
88
|
+
name: 'EM',
|
|
89
|
+
start: /(?:^|\W)(_)[^\s_]/,
|
|
90
|
+
end: /[^\s_](_)(?=$|\W)/
|
|
91
|
+
},
|
|
92
|
+
// Deleted, ~strike this though~
|
|
93
|
+
{
|
|
94
|
+
name: 'DL',
|
|
95
|
+
start: /(?:^|[\W_])(~)[^\s~]/,
|
|
96
|
+
end: /[^\s~](~)(?=$|[\W_])/
|
|
97
|
+
},
|
|
98
|
+
// Code block `this is monospace`
|
|
99
|
+
{
|
|
100
|
+
name: 'CO',
|
|
101
|
+
start: /(?:^|\W)(`)[^`]/,
|
|
102
|
+
end: /[^`](`)(?=$|\W)/
|
|
103
|
+
}
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
// Relative weights of formatting spans. Greater index in array means greater weight.
|
|
107
|
+
const FMT_WEIGHT = ['QQ'];
|
|
108
|
+
|
|
109
|
+
// RegExps for entity extraction (RF = reference)
|
|
110
|
+
const ENTITY_TYPES = [
|
|
111
|
+
// URLs
|
|
112
|
+
{
|
|
113
|
+
name: 'LN',
|
|
114
|
+
dataName: 'url',
|
|
115
|
+
pack: function(val) {
|
|
116
|
+
// Check if the protocol is specified, if not use http
|
|
117
|
+
if (!/^[a-z]+:\/\//i.test(val)) {
|
|
118
|
+
val = 'http://' + val;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
url: val
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
re: /(?:(?:https?|ftp):\/\/|www\.|ftp\.)[-A-Z0-9+&@#\/%=~_|$?!:,.]*[A-Z0-9+&@#\/%=~_|$]/ig
|
|
125
|
+
},
|
|
126
|
+
// Mentions @user (must be 2 or more characters)
|
|
127
|
+
{
|
|
128
|
+
name: 'MN',
|
|
129
|
+
dataName: 'val',
|
|
130
|
+
pack: function(val) {
|
|
131
|
+
return {
|
|
132
|
+
val: val.slice(1)
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
re: /\B@([\p{L}\p{N}][._\p{L}\p{N}]*[\p{L}\p{N}])/ug
|
|
136
|
+
},
|
|
137
|
+
// Hashtags #hashtag, like metion 2 or more characters.
|
|
138
|
+
{
|
|
139
|
+
name: 'HT',
|
|
140
|
+
dataName: 'val',
|
|
141
|
+
pack: function(val) {
|
|
142
|
+
return {
|
|
143
|
+
val: val.slice(1)
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
re: /\B#([\p{L}\p{N}][._\p{L}\p{N}]*[\p{L}\p{N}])/ug
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
// HTML tag name suggestions
|
|
151
|
+
const FORMAT_TAGS = {
|
|
152
|
+
AU: {
|
|
153
|
+
html_tag: 'audio',
|
|
154
|
+
md_tag: undefined,
|
|
155
|
+
isVoid: false
|
|
156
|
+
},
|
|
157
|
+
BN: {
|
|
158
|
+
html_tag: 'button',
|
|
159
|
+
md_tag: undefined,
|
|
160
|
+
isVoid: false
|
|
161
|
+
},
|
|
162
|
+
BR: {
|
|
163
|
+
html_tag: 'br',
|
|
164
|
+
md_tag: '\n',
|
|
165
|
+
isVoid: true
|
|
166
|
+
},
|
|
167
|
+
CO: {
|
|
168
|
+
html_tag: 'tt',
|
|
169
|
+
md_tag: '`',
|
|
170
|
+
isVoid: false
|
|
171
|
+
},
|
|
172
|
+
DL: {
|
|
173
|
+
html_tag: 'del',
|
|
174
|
+
md_tag: '~',
|
|
175
|
+
isVoid: false
|
|
176
|
+
},
|
|
177
|
+
EM: {
|
|
178
|
+
html_tag: 'i',
|
|
179
|
+
md_tag: '_',
|
|
180
|
+
isVoid: false
|
|
181
|
+
},
|
|
182
|
+
EX: {
|
|
183
|
+
html_tag: '',
|
|
184
|
+
md_tag: undefined,
|
|
185
|
+
isVoid: true
|
|
186
|
+
},
|
|
187
|
+
FM: {
|
|
188
|
+
html_tag: 'div',
|
|
189
|
+
md_tag: undefined,
|
|
190
|
+
isVoid: false
|
|
191
|
+
},
|
|
192
|
+
HD: {
|
|
193
|
+
html_tag: '',
|
|
194
|
+
md_tag: undefined,
|
|
195
|
+
isVoid: false
|
|
196
|
+
},
|
|
197
|
+
HL: {
|
|
198
|
+
html_tag: 'span',
|
|
199
|
+
md_tag: undefined,
|
|
200
|
+
isVoid: false
|
|
201
|
+
},
|
|
202
|
+
HT: {
|
|
203
|
+
html_tag: 'a',
|
|
204
|
+
md_tag: undefined,
|
|
205
|
+
isVoid: false
|
|
206
|
+
},
|
|
207
|
+
IM: {
|
|
208
|
+
html_tag: 'img',
|
|
209
|
+
md_tag: undefined,
|
|
210
|
+
isVoid: false
|
|
211
|
+
},
|
|
212
|
+
LN: {
|
|
213
|
+
html_tag: 'a',
|
|
214
|
+
md_tag: undefined,
|
|
215
|
+
isVoid: false
|
|
216
|
+
},
|
|
217
|
+
MN: {
|
|
218
|
+
html_tag: 'a',
|
|
219
|
+
md_tag: undefined,
|
|
220
|
+
isVoid: false
|
|
221
|
+
},
|
|
222
|
+
RW: {
|
|
223
|
+
html_tag: 'div',
|
|
224
|
+
md_tag: undefined,
|
|
225
|
+
isVoid: false,
|
|
226
|
+
},
|
|
227
|
+
QQ: {
|
|
228
|
+
html_tag: 'div',
|
|
229
|
+
md_tag: undefined,
|
|
230
|
+
isVoid: false
|
|
231
|
+
},
|
|
232
|
+
ST: {
|
|
233
|
+
html_tag: 'b',
|
|
234
|
+
md_tag: '*',
|
|
235
|
+
isVoid: false
|
|
236
|
+
},
|
|
237
|
+
VC: {
|
|
238
|
+
html_tag: 'div',
|
|
239
|
+
md_tag: undefined,
|
|
240
|
+
isVoid: false
|
|
241
|
+
},
|
|
242
|
+
VD: {
|
|
243
|
+
html_tag: 'video',
|
|
244
|
+
md_tag: undefined,
|
|
245
|
+
isVoid: false
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Convert base64-encoded string into Blob.
|
|
250
|
+
function base64toObjectUrl(b64, contentType, logger) {
|
|
251
|
+
if (!b64) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const bin = atob(b64);
|
|
257
|
+
const length = bin.length;
|
|
258
|
+
const buf = new ArrayBuffer(length);
|
|
259
|
+
const arr = new Uint8Array(buf);
|
|
260
|
+
for (let i = 0; i < length; i++) {
|
|
261
|
+
arr[i] = bin.charCodeAt(i);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return URL.createObjectURL(new Blob([buf], {
|
|
265
|
+
type: contentType
|
|
266
|
+
}));
|
|
267
|
+
} catch (err) {
|
|
268
|
+
if (logger) {
|
|
269
|
+
logger("Drafty: failed to convert object.", err.message);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function base64toDataUrl(b64, contentType) {
|
|
277
|
+
if (!b64) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
contentType = contentType || 'image/jpeg';
|
|
281
|
+
return 'data:' + contentType + ';base64,' + b64;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Helpers for converting Drafty to HTML.
|
|
285
|
+
const DECORATORS = {
|
|
286
|
+
// Visial styles
|
|
287
|
+
ST: {
|
|
288
|
+
open: _ => '<b>',
|
|
289
|
+
close: _ => '</b>'
|
|
290
|
+
},
|
|
291
|
+
EM: {
|
|
292
|
+
open: _ => '<i>',
|
|
293
|
+
close: _ => '</i>'
|
|
294
|
+
},
|
|
295
|
+
DL: {
|
|
296
|
+
open: _ => '<del>',
|
|
297
|
+
close: _ => '</del>'
|
|
298
|
+
},
|
|
299
|
+
CO: {
|
|
300
|
+
open: _ => '<tt>',
|
|
301
|
+
close: _ => '</tt>'
|
|
302
|
+
},
|
|
303
|
+
// Line break
|
|
304
|
+
BR: {
|
|
305
|
+
open: _ => '<br/>',
|
|
306
|
+
close: _ => ''
|
|
307
|
+
},
|
|
308
|
+
// Hidden element
|
|
309
|
+
HD: {
|
|
310
|
+
open: _ => '',
|
|
311
|
+
close: _ => ''
|
|
312
|
+
},
|
|
313
|
+
// Highlighted element.
|
|
314
|
+
HL: {
|
|
315
|
+
open: _ => '<span style="color:teal">',
|
|
316
|
+
close: _ => '</span>'
|
|
317
|
+
},
|
|
318
|
+
// Link (URL)
|
|
319
|
+
LN: {
|
|
320
|
+
open: (data) => {
|
|
321
|
+
return '<a href="' + data.url + '">';
|
|
322
|
+
},
|
|
323
|
+
close: _ => '</a>',
|
|
324
|
+
props: (data) => {
|
|
325
|
+
return data ? {
|
|
326
|
+
href: data.url,
|
|
327
|
+
target: '_blank'
|
|
328
|
+
} : null;
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
// Mention
|
|
332
|
+
MN: {
|
|
333
|
+
open: (data) => {
|
|
334
|
+
return '<a href="#' + data.val + '">';
|
|
335
|
+
},
|
|
336
|
+
close: _ => '</a>',
|
|
337
|
+
props: (data) => {
|
|
338
|
+
return data ? {
|
|
339
|
+
id: data.val
|
|
340
|
+
} : null;
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
// Hashtag
|
|
344
|
+
HT: {
|
|
345
|
+
open: (data) => {
|
|
346
|
+
return '<a href="#' + data.val + '">';
|
|
347
|
+
},
|
|
348
|
+
close: _ => '</a>',
|
|
349
|
+
props: (data) => {
|
|
350
|
+
return data ? {
|
|
351
|
+
id: data.val
|
|
352
|
+
} : null;
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
// Button
|
|
356
|
+
BN: {
|
|
357
|
+
open: _ => '<button>',
|
|
358
|
+
close: _ => '</button>',
|
|
359
|
+
props: (data) => {
|
|
360
|
+
return data ? {
|
|
361
|
+
'data-act': data.act,
|
|
362
|
+
'data-val': data.val,
|
|
363
|
+
'data-name': data.name,
|
|
364
|
+
'data-ref': data.ref
|
|
365
|
+
} : null;
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
// Audio recording
|
|
369
|
+
AU: {
|
|
370
|
+
open: (data) => {
|
|
371
|
+
const url = data.ref || base64toObjectUrl(data.val, data.mime, Drafty.logger);
|
|
372
|
+
return '<audio controls src="' + url + '">';
|
|
373
|
+
},
|
|
374
|
+
close: _ => '</audio>',
|
|
375
|
+
props: (data) => {
|
|
376
|
+
if (!data) return null;
|
|
377
|
+
return {
|
|
378
|
+
// Embedded data or external link.
|
|
379
|
+
src: data.ref || base64toObjectUrl(data.val, data.mime, Drafty.logger),
|
|
380
|
+
'data-preload': data.ref ? 'metadata' : 'auto',
|
|
381
|
+
'data-duration': data.duration,
|
|
382
|
+
'data-name': data.name,
|
|
383
|
+
'data-size': data.val ? ((data.val.length * 0.75) | 0) : (data.size | 0),
|
|
384
|
+
'data-mime': data.mime,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
// Image
|
|
389
|
+
IM: {
|
|
390
|
+
open: data => {
|
|
391
|
+
// Don't use data.ref for preview: it's a security risk.
|
|
392
|
+
const tmpPreviewUrl = base64toDataUrl(data._tempPreview, data.mime);
|
|
393
|
+
const previewUrl = base64toObjectUrl(data.val, data.mime, Drafty.logger);
|
|
394
|
+
const downloadUrl = data.ref || previewUrl;
|
|
395
|
+
return (data.name ? '<a href="' + downloadUrl + '" download="' + data.name + '">' : '') +
|
|
396
|
+
'<img src="' + (tmpPreviewUrl || previewUrl) + '"' +
|
|
397
|
+
(data.width ? ' width="' + data.width + '"' : '') +
|
|
398
|
+
(data.height ? ' height="' + data.height + '"' : '') + ' border="0" />';
|
|
399
|
+
},
|
|
400
|
+
close: data => {
|
|
401
|
+
return (data.name ? '</a>' : '');
|
|
402
|
+
},
|
|
403
|
+
props: data => {
|
|
404
|
+
if (!data) return null;
|
|
405
|
+
return {
|
|
406
|
+
// Temporary preview, or permanent preview, or external link.
|
|
407
|
+
src: base64toDataUrl(data._tempPreview, data.mime) ||
|
|
408
|
+
data.ref || base64toObjectUrl(data.val, data.mime, Drafty.logger),
|
|
409
|
+
title: data.name,
|
|
410
|
+
alt: data.name,
|
|
411
|
+
'data-width': data.width,
|
|
412
|
+
'data-height': data.height,
|
|
413
|
+
'data-name': data.name,
|
|
414
|
+
'data-size': data.ref ? (data.size | 0) : (data.val ? ((data.val.length * 0.75) | 0) : (data.size | 0)),
|
|
415
|
+
'data-mime': data.mime,
|
|
416
|
+
};
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
// Form - structured layout of elements.
|
|
420
|
+
FM: {
|
|
421
|
+
open: _ => '<div>',
|
|
422
|
+
close: _ => '</div>'
|
|
423
|
+
},
|
|
424
|
+
// Row: logic grouping of elements
|
|
425
|
+
RW: {
|
|
426
|
+
open: _ => '<div>',
|
|
427
|
+
close: _ => '</div>'
|
|
428
|
+
},
|
|
429
|
+
// Quoted block.
|
|
430
|
+
QQ: {
|
|
431
|
+
open: _ => '<div>',
|
|
432
|
+
close: _ => '</div>',
|
|
433
|
+
props: (data) => {
|
|
434
|
+
return data ? {} : null;
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
// Video call
|
|
438
|
+
VC: {
|
|
439
|
+
open: _ => '<div>',
|
|
440
|
+
close: _ => '</div>',
|
|
441
|
+
props: data => {
|
|
442
|
+
if (!data) return {};
|
|
443
|
+
return {
|
|
444
|
+
'data-duration': data.duration,
|
|
445
|
+
'data-state': data.state,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
// Video.
|
|
450
|
+
VD: {
|
|
451
|
+
open: data => {
|
|
452
|
+
const tmpPreviewUrl = base64toDataUrl(data._tempPreview, data.mime);
|
|
453
|
+
const previewUrl = data.ref || base64toObjectUrl(data.preview, data.premime || 'image/jpeg', Drafty.logger);
|
|
454
|
+
return '<img src="' + (tmpPreviewUrl || previewUrl) + '"' +
|
|
455
|
+
(data.width ? ' width="' + data.width + '"' : '') +
|
|
456
|
+
(data.height ? ' height="' + data.height + '"' : '') + ' border="0" />';
|
|
457
|
+
},
|
|
458
|
+
close: _ => '',
|
|
459
|
+
props: data => {
|
|
460
|
+
if (!data) return null;
|
|
461
|
+
const poster = data.preref || base64toObjectUrl(data.preview, data.premime || 'image/jpeg', Drafty.logger);
|
|
462
|
+
return {
|
|
463
|
+
// Embedded data or external link.
|
|
464
|
+
src: poster,
|
|
465
|
+
'data-src': data.ref || base64toObjectUrl(data.val, data.mime, Drafty.logger),
|
|
466
|
+
'data-width': data.width,
|
|
467
|
+
'data-height': data.height,
|
|
468
|
+
'data-preload': data.ref ? 'metadata' : 'auto',
|
|
469
|
+
'data-preview': poster,
|
|
470
|
+
'data-duration': data.duration | 0,
|
|
471
|
+
'data-name': data.name,
|
|
472
|
+
'data-size': data.ref ? (data.size | 0) : (data.val ? ((data.val.length * 0.75) | 0) : (data.size | 0)),
|
|
473
|
+
'data-mime': data.mime,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* The main object which performs all the formatting actions.
|
|
481
|
+
* @class Drafty
|
|
482
|
+
* @constructor
|
|
483
|
+
*/
|
|
484
|
+
const Drafty = function() {
|
|
485
|
+
this.txt = '';
|
|
486
|
+
this.fmt = [];
|
|
487
|
+
this.ent = [];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Initialize Drafty document to a plain text string.
|
|
492
|
+
*
|
|
493
|
+
* @param {string} plainText - string to use as Drafty content.
|
|
494
|
+
*
|
|
495
|
+
* @returns new Drafty document or null is plainText is not a string or undefined.
|
|
496
|
+
*/
|
|
497
|
+
Drafty.init = function(plainText) {
|
|
498
|
+
if (typeof plainText == 'undefined') {
|
|
499
|
+
plainText = '';
|
|
500
|
+
} else if (typeof plainText != 'string') {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
txt: plainText
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Parse plain text into Drafty document.
|
|
511
|
+
* @memberof Drafty
|
|
512
|
+
* @static
|
|
513
|
+
*
|
|
514
|
+
* @param {string} content - plain-text content to parse.
|
|
515
|
+
* @return {Drafty} parsed document or null if the source is not plain text.
|
|
516
|
+
*/
|
|
517
|
+
Drafty.parse = function(content) {
|
|
518
|
+
// Make sure we are parsing strings only.
|
|
519
|
+
if (typeof content != 'string') {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Split text into lines. It makes further processing easier.
|
|
524
|
+
const lines = content.split(/\r?\n/);
|
|
525
|
+
|
|
526
|
+
// Holds entities referenced from text
|
|
527
|
+
const entityMap = [];
|
|
528
|
+
const entityIndex = {};
|
|
529
|
+
|
|
530
|
+
// Processing lines one by one, hold intermediate result in blx.
|
|
531
|
+
const blx = [];
|
|
532
|
+
lines.forEach((line) => {
|
|
533
|
+
let spans = [];
|
|
534
|
+
let entities;
|
|
535
|
+
|
|
536
|
+
// Find formatted spans in the string.
|
|
537
|
+
// Try to match each style.
|
|
538
|
+
INLINE_STYLES.forEach((tag) => {
|
|
539
|
+
// Each style could be matched multiple times.
|
|
540
|
+
spans = spans.concat(spannify(line, tag.start, tag.end, tag.name));
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
let block;
|
|
544
|
+
if (spans.length == 0) {
|
|
545
|
+
block = {
|
|
546
|
+
txt: line
|
|
547
|
+
};
|
|
548
|
+
} else {
|
|
549
|
+
// Sort spans by style occurence early -> late, then by length: first long then short.
|
|
550
|
+
spans.sort((a, b) => {
|
|
551
|
+
const diff = a.at - b.at;
|
|
552
|
+
return diff != 0 ? diff : b.end - a.end;
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// Convert an array of possibly overlapping spans into a tree.
|
|
556
|
+
spans = toSpanTree(spans);
|
|
557
|
+
|
|
558
|
+
// Build a tree representation of the entire string, not
|
|
559
|
+
// just the formatted parts.
|
|
560
|
+
const chunks = chunkify(line, 0, line.length, spans);
|
|
561
|
+
|
|
562
|
+
const drafty = draftify(chunks, 0);
|
|
563
|
+
|
|
564
|
+
block = {
|
|
565
|
+
txt: drafty.txt,
|
|
566
|
+
fmt: drafty.fmt
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Extract entities from the cleaned up string.
|
|
571
|
+
entities = extractEntities(block.txt);
|
|
572
|
+
if (entities.length > 0) {
|
|
573
|
+
const ranges = [];
|
|
574
|
+
for (let i in entities) {
|
|
575
|
+
// {offset: match['index'], unique: match[0], len: match[0].length, data: ent.packer(), type: ent.name}
|
|
576
|
+
const entity = entities[i];
|
|
577
|
+
let index = entityIndex[entity.unique];
|
|
578
|
+
if (!index) {
|
|
579
|
+
index = entityMap.length;
|
|
580
|
+
entityIndex[entity.unique] = index;
|
|
581
|
+
entityMap.push({
|
|
582
|
+
tp: entity.type,
|
|
583
|
+
data: entity.data
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
ranges.push({
|
|
587
|
+
at: entity.offset,
|
|
588
|
+
len: entity.len,
|
|
589
|
+
key: index
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
block.ent = ranges;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
blx.push(block);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const result = {
|
|
599
|
+
txt: ''
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// Merge lines and save line breaks as BR inline formatting.
|
|
603
|
+
if (blx.length > 0) {
|
|
604
|
+
result.txt = blx[0].txt;
|
|
605
|
+
result.fmt = (blx[0].fmt || []).concat(blx[0].ent || []);
|
|
606
|
+
|
|
607
|
+
if (result.fmt.length) {
|
|
608
|
+
const segments = segmenter.segment(result.txt);
|
|
609
|
+
for (const ele of result.fmt) {
|
|
610
|
+
({
|
|
611
|
+
at: ele.at,
|
|
612
|
+
len: ele.len
|
|
613
|
+
} =
|
|
614
|
+
toGraphemeValues(ele, segments, result.txt));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
for (let i = 1; i < blx.length; i++) {
|
|
619
|
+
const block = blx[i];
|
|
620
|
+
const offset = stringToGraphemes(result.txt).length + 1;
|
|
621
|
+
|
|
622
|
+
result.fmt.push({
|
|
623
|
+
tp: 'BR',
|
|
624
|
+
len: 1,
|
|
625
|
+
at: offset - 1
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
let segments = {};
|
|
629
|
+
|
|
630
|
+
result.txt += ' ' + block.txt;
|
|
631
|
+
if (block.fmt) {
|
|
632
|
+
segments = segmenter.segment(block.txt);
|
|
633
|
+
result.fmt = result.fmt.concat(
|
|
634
|
+
block.fmt.map((s) => {
|
|
635
|
+
const {
|
|
636
|
+
at: correctAt,
|
|
637
|
+
len: correctLen
|
|
638
|
+
} =
|
|
639
|
+
toGraphemeValues(s, segments, block.txt);
|
|
640
|
+
s.at = correctAt + offset;
|
|
641
|
+
s.len = correctLen;
|
|
642
|
+
return s;
|
|
643
|
+
})
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
if (block.ent) {
|
|
647
|
+
if (isEmptyObject(segments)) {
|
|
648
|
+
segments = segmenter.segment(block.txt);
|
|
649
|
+
}
|
|
650
|
+
result.fmt = result.fmt.concat(
|
|
651
|
+
block.ent.map((s) => {
|
|
652
|
+
const {
|
|
653
|
+
at: correctAt,
|
|
654
|
+
len: correctLen
|
|
655
|
+
} =
|
|
656
|
+
toGraphemeValues(s, segments, block.txt);
|
|
657
|
+
s.at = correctAt + offset;
|
|
658
|
+
s.len = correctLen;
|
|
659
|
+
return s;
|
|
660
|
+
})
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (result.fmt.length == 0) {
|
|
666
|
+
delete result.fmt;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (entityMap.length > 0) {
|
|
670
|
+
result.ent = entityMap;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return result;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Append one Drafty document to another.
|
|
678
|
+
*
|
|
679
|
+
* @param {Drafty} first - Drafty document to append to.
|
|
680
|
+
* @param {Drafty|string} second - Drafty document or string being appended.
|
|
681
|
+
*
|
|
682
|
+
* @return {Drafty} first document with the second appended to it.
|
|
683
|
+
*/
|
|
684
|
+
Drafty.append = function(first, second) {
|
|
685
|
+
if (!first) {
|
|
686
|
+
return second;
|
|
687
|
+
}
|
|
688
|
+
if (!second) {
|
|
689
|
+
return first;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
first.txt = first.txt || '';
|
|
693
|
+
const len = stringToGraphemes(first.txt).length;
|
|
694
|
+
|
|
695
|
+
if (typeof second == 'string') {
|
|
696
|
+
first.txt += second;
|
|
697
|
+
} else if (second.txt) {
|
|
698
|
+
first.txt += second.txt;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (Array.isArray(second.fmt)) {
|
|
702
|
+
first.fmt = first.fmt || [];
|
|
703
|
+
if (Array.isArray(second.ent)) {
|
|
704
|
+
first.ent = first.ent || [];
|
|
705
|
+
}
|
|
706
|
+
second.fmt.forEach(src => {
|
|
707
|
+
const fmt = {
|
|
708
|
+
at: (src.at | 0) + len,
|
|
709
|
+
len: src.len | 0
|
|
710
|
+
};
|
|
711
|
+
// Special case for the outside of the normal rendering flow styles.
|
|
712
|
+
if (src.at == -1) {
|
|
713
|
+
fmt.at = -1;
|
|
714
|
+
fmt.len = 0;
|
|
715
|
+
}
|
|
716
|
+
if (src.tp) {
|
|
717
|
+
fmt.tp = src.tp;
|
|
718
|
+
} else {
|
|
719
|
+
fmt.key = first.ent.length;
|
|
720
|
+
first.ent.push(second.ent[src.key || 0]);
|
|
721
|
+
}
|
|
722
|
+
first.fmt.push(fmt);
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return first;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Description of an image to attach.
|
|
731
|
+
* @typedef {Object} ImageDesc
|
|
732
|
+
* @memberof Drafty
|
|
733
|
+
*
|
|
734
|
+
* @property {string} mime - mime-type of the image, e.g. "image/png".
|
|
735
|
+
* @property {string} refurl - reference to the content. Could be null/undefined.
|
|
736
|
+
* @property {string} bits - base64-encoded image content. Could be null/undefined.
|
|
737
|
+
* @property {string} preview - base64-encoded thumbnail of the image.
|
|
738
|
+
* @property {integer} width - width of the image.
|
|
739
|
+
* @property {integer} height - height of the image.
|
|
740
|
+
* @property {string} filename - file name suggestion for downloading the image.
|
|
741
|
+
* @property {integer} size - size of the image in bytes. Treat is as an untrusted hint.
|
|
742
|
+
* @property {string} _tempPreview - base64-encoded image preview used during upload process; not serializable.
|
|
743
|
+
* @property {Promise} urlPromise - Promise which returns content URL when resolved.
|
|
744
|
+
*/
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Insert inline image into Drafty document.
|
|
748
|
+
* @memberof Drafty
|
|
749
|
+
* @static
|
|
750
|
+
*
|
|
751
|
+
* @param {Drafty} content - document to add image to.
|
|
752
|
+
* @param {integer} at - index where the object is inserted. The length of the image is always 1.
|
|
753
|
+
* @param {ImageDesc} imageDesc - object with image paramenets and data.
|
|
754
|
+
*
|
|
755
|
+
* @return {Drafty} updated document.
|
|
756
|
+
*/
|
|
757
|
+
Drafty.insertImage = function(content, at, imageDesc) {
|
|
758
|
+
content = content || {
|
|
759
|
+
txt: ' '
|
|
760
|
+
};
|
|
761
|
+
content.ent = content.ent || [];
|
|
762
|
+
content.fmt = content.fmt || [];
|
|
763
|
+
|
|
764
|
+
content.fmt.push({
|
|
765
|
+
at: at | 0,
|
|
766
|
+
len: 1,
|
|
767
|
+
key: content.ent.length
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
const ex = {
|
|
771
|
+
tp: 'IM',
|
|
772
|
+
data: {
|
|
773
|
+
mime: imageDesc.mime,
|
|
774
|
+
ref: imageDesc.refurl,
|
|
775
|
+
val: imageDesc.bits || imageDesc.preview,
|
|
776
|
+
width: imageDesc.width,
|
|
777
|
+
height: imageDesc.height,
|
|
778
|
+
name: imageDesc.filename,
|
|
779
|
+
size: imageDesc.size | 0,
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
if (imageDesc.urlPromise) {
|
|
784
|
+
ex.data._tempPreview = imageDesc._tempPreview;
|
|
785
|
+
ex.data._processing = true;
|
|
786
|
+
imageDesc.urlPromise.then(
|
|
787
|
+
url => {
|
|
788
|
+
ex.data.ref = url;
|
|
789
|
+
ex.data._tempPreview = undefined;
|
|
790
|
+
ex.data._processing = undefined;
|
|
791
|
+
},
|
|
792
|
+
_ => {
|
|
793
|
+
// Catch the error, otherwise it will appear in the console.
|
|
794
|
+
ex.data._processing = undefined;
|
|
795
|
+
}
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
content.ent.push(ex);
|
|
800
|
+
|
|
801
|
+
return content;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Description of a video to attach.
|
|
806
|
+
* @typedef {Object} VideoDesc
|
|
807
|
+
* @memberof Drafty
|
|
808
|
+
*
|
|
809
|
+
* @property {string} mime - mime-type of the video, e.g. "video/mpeg".
|
|
810
|
+
* @property {string} refurl - reference to the content. Could be null/undefined.
|
|
811
|
+
* @property {string} bits - in-band base64-encoded image data. Could be null/undefined.
|
|
812
|
+
* @property {string} preview - base64-encoded screencapture from the video. Could be null/undefined.
|
|
813
|
+
* @property {string} preref - reference to screencapture from the video. Could be null/undefined.
|
|
814
|
+
* @property {integer} width - width of the video.
|
|
815
|
+
* @property {integer} height - height of the video.
|
|
816
|
+
* @property {integer} duration - duration of the video.
|
|
817
|
+
* @property {string} filename - file name suggestion for downloading the video.
|
|
818
|
+
* @property {integer} size - size of the video in bytes. Treat is as an untrusted hint.
|
|
819
|
+
* @property {string} _tempPreview - base64-encoded screencapture used during upload process; not serializable.
|
|
820
|
+
* @property {Promise} urlPromise - array of two promises, which return URLs of video and preview uploads correspondingly
|
|
821
|
+
* (either could be null).
|
|
822
|
+
*/
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Insert inline image into Drafty document.
|
|
826
|
+
* @memberof Drafty
|
|
827
|
+
* @static
|
|
828
|
+
*
|
|
829
|
+
* @param {Drafty} content - document to add video to.
|
|
830
|
+
* @param {integer} at - index where the object is inserted. The length of the video is always 1.
|
|
831
|
+
* @param {VideoDesc} videoDesc - object with video paramenets and data.
|
|
832
|
+
*
|
|
833
|
+
* @return {Drafty} updated document.
|
|
834
|
+
*/
|
|
835
|
+
Drafty.insertVideo = function(content, at, videoDesc) {
|
|
836
|
+
content = content || {
|
|
837
|
+
txt: ' '
|
|
838
|
+
};
|
|
839
|
+
content.ent = content.ent || [];
|
|
840
|
+
content.fmt = content.fmt || [];
|
|
841
|
+
|
|
842
|
+
content.fmt.push({
|
|
843
|
+
at: at | 0,
|
|
844
|
+
len: 1,
|
|
845
|
+
key: content.ent.length
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
const ex = {
|
|
849
|
+
tp: 'VD',
|
|
850
|
+
data: {
|
|
851
|
+
mime: videoDesc.mime,
|
|
852
|
+
ref: videoDesc.refurl,
|
|
853
|
+
val: videoDesc.bits,
|
|
854
|
+
preref: videoDesc.preref,
|
|
855
|
+
preview: videoDesc.preview,
|
|
856
|
+
width: videoDesc.width,
|
|
857
|
+
height: videoDesc.height,
|
|
858
|
+
duration: videoDesc.duration | 0,
|
|
859
|
+
name: videoDesc.filename,
|
|
860
|
+
size: videoDesc.size | 0,
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
if (videoDesc.urlPromise) {
|
|
865
|
+
ex.data._tempPreview = videoDesc._tempPreview;
|
|
866
|
+
ex.data._processing = true;
|
|
867
|
+
videoDesc.urlPromise.then(
|
|
868
|
+
urls => {
|
|
869
|
+
ex.data.ref = urls[0];
|
|
870
|
+
ex.data.preref = urls[1];
|
|
871
|
+
ex.data._tempPreview = undefined;
|
|
872
|
+
ex.data._processing = undefined;
|
|
873
|
+
},
|
|
874
|
+
_ => {
|
|
875
|
+
// Catch the error, otherwise it will appear in the console.
|
|
876
|
+
ex.data._processing = undefined;
|
|
877
|
+
}
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
content.ent.push(ex);
|
|
882
|
+
|
|
883
|
+
return content;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Description of an audio recording to attach.
|
|
888
|
+
* @typedef {Object} AudioDesc
|
|
889
|
+
* @memberof Drafty
|
|
890
|
+
*
|
|
891
|
+
* @property {string} mime - mime-type of the audio, e.g. "audio/ogg".
|
|
892
|
+
* @property {string} refurl - reference to the content. Could be null/undefined.
|
|
893
|
+
* @property {string} bits - base64-encoded audio content. Could be null/undefined.
|
|
894
|
+
* @property {integer} duration - duration of the record in milliseconds.
|
|
895
|
+
* @property {string} preview - base64 encoded short array of amplitude values 0..100.
|
|
896
|
+
* @property {string} filename - file name suggestion for downloading the audio.
|
|
897
|
+
* @property {integer} size - size of the recording in bytes. Treat is as an untrusted hint.
|
|
898
|
+
* @property {Promise} urlPromise - Promise which returns content URL when resolved.
|
|
899
|
+
*/
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Insert audio recording into Drafty document.
|
|
903
|
+
* @memberof Drafty
|
|
904
|
+
* @static
|
|
905
|
+
*
|
|
906
|
+
* @param {Drafty} content - document to add audio record to.
|
|
907
|
+
* @param {integer} at - index where the object is inserted. The length of the record is always 1.
|
|
908
|
+
* @param {AudioDesc} audioDesc - object with the audio paramenets and data.
|
|
909
|
+
*
|
|
910
|
+
* @return {Drafty} updated document.
|
|
911
|
+
*/
|
|
912
|
+
Drafty.insertAudio = function(content, at, audioDesc) {
|
|
913
|
+
content = content || {
|
|
914
|
+
txt: ' '
|
|
915
|
+
};
|
|
916
|
+
content.ent = content.ent || [];
|
|
917
|
+
content.fmt = content.fmt || [];
|
|
918
|
+
|
|
919
|
+
content.fmt.push({
|
|
920
|
+
at: at | 0,
|
|
921
|
+
len: 1,
|
|
922
|
+
key: content.ent.length
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
const ex = {
|
|
926
|
+
tp: 'AU',
|
|
927
|
+
data: {
|
|
928
|
+
mime: audioDesc.mime,
|
|
929
|
+
val: audioDesc.bits,
|
|
930
|
+
duration: audioDesc.duration | 0,
|
|
931
|
+
preview: audioDesc.preview,
|
|
932
|
+
name: audioDesc.filename,
|
|
933
|
+
size: audioDesc.size | 0,
|
|
934
|
+
ref: audioDesc.refurl
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
if (audioDesc.urlPromise) {
|
|
939
|
+
ex.data._processing = true;
|
|
940
|
+
audioDesc.urlPromise.then(
|
|
941
|
+
url => {
|
|
942
|
+
ex.data.ref = url;
|
|
943
|
+
ex.data._processing = undefined;
|
|
944
|
+
},
|
|
945
|
+
_ => {
|
|
946
|
+
// Catch the error, otherwise it will appear in the console.
|
|
947
|
+
ex.data._processing = undefined;
|
|
948
|
+
}
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
content.ent.push(ex);
|
|
953
|
+
|
|
954
|
+
return content;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Create a (self-contained) video call Drafty document.
|
|
959
|
+
* @memberof Drafty
|
|
960
|
+
* @static
|
|
961
|
+
* @param {boolean} audioOnly <code>true</code> if the call is initially audio-only.
|
|
962
|
+
* @returns Video Call drafty document.
|
|
963
|
+
*/
|
|
964
|
+
Drafty.videoCall = function(audioOnly) {
|
|
965
|
+
const content = {
|
|
966
|
+
txt: ' ',
|
|
967
|
+
fmt: [{
|
|
968
|
+
at: 0,
|
|
969
|
+
len: 1,
|
|
970
|
+
key: 0
|
|
971
|
+
}],
|
|
972
|
+
ent: [{
|
|
973
|
+
tp: 'VC',
|
|
974
|
+
data: {
|
|
975
|
+
aonly: audioOnly
|
|
976
|
+
},
|
|
977
|
+
}]
|
|
978
|
+
};
|
|
979
|
+
return content;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Update video call (VC) entity with the new status and duration.
|
|
984
|
+
* @memberof Drafty
|
|
985
|
+
* @static
|
|
986
|
+
*
|
|
987
|
+
* @param {Drafty} content - VC document to update.
|
|
988
|
+
* @param {object} params - new video call parameters.
|
|
989
|
+
* @param {string} params.state - state of video call.
|
|
990
|
+
* @param {number} params.duration - duration of the video call in milliseconds.
|
|
991
|
+
*
|
|
992
|
+
* @returns the same document with update applied.
|
|
993
|
+
*/
|
|
994
|
+
Drafty.updateVideoCall = function(content, params) {
|
|
995
|
+
// The video element could be just a format or a format + entity.
|
|
996
|
+
// Must ensure it's the latter first.
|
|
997
|
+
const fmt = ((content || {}).fmt || [])[0];
|
|
998
|
+
if (!fmt) {
|
|
999
|
+
// Unrecognized content.
|
|
1000
|
+
return content;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
let ent;
|
|
1004
|
+
if (fmt.tp == 'VC') {
|
|
1005
|
+
// Just a format, convert to format + entity.
|
|
1006
|
+
delete fmt.tp;
|
|
1007
|
+
fmt.key = 0;
|
|
1008
|
+
ent = {
|
|
1009
|
+
tp: 'VC'
|
|
1010
|
+
};
|
|
1011
|
+
content.ent = [ent];
|
|
1012
|
+
} else {
|
|
1013
|
+
ent = (content.ent || [])[fmt.key | 0];
|
|
1014
|
+
if (!ent || ent.tp != 'VC') {
|
|
1015
|
+
// Not a VC entity.
|
|
1016
|
+
return content;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
ent.data = ent.data || {};
|
|
1020
|
+
Object.assign(ent.data, params);
|
|
1021
|
+
return content;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Create a quote to Drafty document.
|
|
1026
|
+
* @memberof Drafty
|
|
1027
|
+
* @static
|
|
1028
|
+
*
|
|
1029
|
+
* @param {string} header - Quote header (title, etc.).
|
|
1030
|
+
* @param {string} uid - UID of the author to mention.
|
|
1031
|
+
* @param {Drafty} body - Body of the quoted message.
|
|
1032
|
+
*
|
|
1033
|
+
* @returns Reply quote Drafty doc with the quote formatting.
|
|
1034
|
+
*/
|
|
1035
|
+
Drafty.quote = function(header, uid, body) {
|
|
1036
|
+
const quote = Drafty.append(Drafty.appendLineBreak(Drafty.mention(header, uid)), body);
|
|
1037
|
+
|
|
1038
|
+
// Wrap into a quote.
|
|
1039
|
+
quote.fmt.push({
|
|
1040
|
+
at: 0,
|
|
1041
|
+
len: stringToGraphemes(quote.txt).length,
|
|
1042
|
+
tp: 'QQ'
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
return quote;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Create a Drafty document with a mention.
|
|
1050
|
+
*
|
|
1051
|
+
* @param {string} name - mentioned name.
|
|
1052
|
+
* @param {string} uid - mentioned user ID.
|
|
1053
|
+
*
|
|
1054
|
+
* @returns {Drafty} document with the mention.
|
|
1055
|
+
*/
|
|
1056
|
+
Drafty.mention = function(name, uid) {
|
|
1057
|
+
return {
|
|
1058
|
+
txt: name || '',
|
|
1059
|
+
fmt: [{
|
|
1060
|
+
at: 0,
|
|
1061
|
+
len: stringToGraphemes(name || '').length,
|
|
1062
|
+
key: 0
|
|
1063
|
+
}],
|
|
1064
|
+
ent: [{
|
|
1065
|
+
tp: 'MN',
|
|
1066
|
+
data: {
|
|
1067
|
+
val: uid
|
|
1068
|
+
}
|
|
1069
|
+
}]
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Append a link to a Drafty document.
|
|
1075
|
+
*
|
|
1076
|
+
* @param {Drafty} content - Drafty document to append link to.
|
|
1077
|
+
* @param {Object} linkData - Link info in format <code>{txt: 'ankor text', url: 'http://...'}</code>.
|
|
1078
|
+
*
|
|
1079
|
+
* @returns {Drafty} the same document as <code>content</code>.
|
|
1080
|
+
*/
|
|
1081
|
+
Drafty.appendLink = function(content, linkData) {
|
|
1082
|
+
content = content || {
|
|
1083
|
+
txt: ''
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
content.ent = content.ent || [];
|
|
1087
|
+
content.fmt = content.fmt || [];
|
|
1088
|
+
|
|
1089
|
+
content.fmt.push({
|
|
1090
|
+
at: content.txt.length,
|
|
1091
|
+
len: linkData.txt.length,
|
|
1092
|
+
key: content.ent.length
|
|
1093
|
+
});
|
|
1094
|
+
content.txt += linkData.txt;
|
|
1095
|
+
|
|
1096
|
+
const ex = {
|
|
1097
|
+
tp: 'LN',
|
|
1098
|
+
data: {
|
|
1099
|
+
url: linkData.url
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
content.ent.push(ex);
|
|
1103
|
+
|
|
1104
|
+
return content;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Append image to Drafty document.
|
|
1109
|
+
* @memberof Drafty
|
|
1110
|
+
* @static
|
|
1111
|
+
*
|
|
1112
|
+
* @param {Drafty} content - document to add image to.
|
|
1113
|
+
* @param {ImageDesc} imageDesc - object with image paramenets.
|
|
1114
|
+
*
|
|
1115
|
+
* @return {Drafty} updated document.
|
|
1116
|
+
*/
|
|
1117
|
+
Drafty.appendImage = function(content, imageDesc) {
|
|
1118
|
+
content = content || {
|
|
1119
|
+
txt: ''
|
|
1120
|
+
};
|
|
1121
|
+
content.txt += ' ';
|
|
1122
|
+
return Drafty.insertImage(content, content.txt.length - 1, imageDesc);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Append audio recodring to Drafty document.
|
|
1127
|
+
* @memberof Drafty
|
|
1128
|
+
* @static
|
|
1129
|
+
*
|
|
1130
|
+
* @param {Drafty} content - document to add recording to.
|
|
1131
|
+
* @param {AudioDesc} audioDesc - object with audio data.
|
|
1132
|
+
*
|
|
1133
|
+
* @return {Drafty} updated document.
|
|
1134
|
+
*/
|
|
1135
|
+
Drafty.appendAudio = function(content, audioDesc) {
|
|
1136
|
+
content = content || {
|
|
1137
|
+
txt: ''
|
|
1138
|
+
};
|
|
1139
|
+
content.txt += ' ';
|
|
1140
|
+
return Drafty.insertAudio(content, content.txt.length - 1, audioDesc);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Description of a file to attach.
|
|
1145
|
+
* @typedef {Object} AttachmentDesc
|
|
1146
|
+
* @memberof Drafty
|
|
1147
|
+
*
|
|
1148
|
+
* @property {string} mime - mime-type of the attachment, e.g. "application/octet-stream"
|
|
1149
|
+
* @property {string} data - base64-encoded in-band content of small attachments. Could be null/undefined.
|
|
1150
|
+
* @property {string} filename - file name suggestion for downloading the attachment.
|
|
1151
|
+
* @property {integer} size - size of the file in bytes. Treat is as an untrusted hint.
|
|
1152
|
+
* @property {string} refurl - reference to the out-of-band content. Could be null/undefined.
|
|
1153
|
+
* @property {Promise} urlPromise - Promise which returns content URL when resolved.
|
|
1154
|
+
*/
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Attach file to Drafty content. Either as a blob or as a reference.
|
|
1158
|
+
* @memberof Drafty
|
|
1159
|
+
* @static
|
|
1160
|
+
*
|
|
1161
|
+
* @param {Drafty} content - document to attach file to.
|
|
1162
|
+
* @param {AttachmentDesc} object - containing attachment description and data.
|
|
1163
|
+
*
|
|
1164
|
+
* @return {Drafty} updated document.
|
|
1165
|
+
*/
|
|
1166
|
+
Drafty.attachFile = function(content, attachmentDesc) {
|
|
1167
|
+
content = content || {
|
|
1168
|
+
txt: ''
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
content.ent = content.ent || [];
|
|
1172
|
+
content.fmt = content.fmt || [];
|
|
1173
|
+
|
|
1174
|
+
content.fmt.push({
|
|
1175
|
+
at: -1,
|
|
1176
|
+
len: 0,
|
|
1177
|
+
key: content.ent.length
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
const ex = {
|
|
1181
|
+
tp: 'EX',
|
|
1182
|
+
data: {
|
|
1183
|
+
mime: attachmentDesc.mime,
|
|
1184
|
+
val: attachmentDesc.data,
|
|
1185
|
+
name: attachmentDesc.filename,
|
|
1186
|
+
ref: attachmentDesc.refurl,
|
|
1187
|
+
size: attachmentDesc.size | 0
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (attachmentDesc.urlPromise) {
|
|
1191
|
+
ex.data._processing = true;
|
|
1192
|
+
attachmentDesc.urlPromise.then(
|
|
1193
|
+
url => {
|
|
1194
|
+
ex.data.ref = url;
|
|
1195
|
+
ex.data._processing = undefined;
|
|
1196
|
+
},
|
|
1197
|
+
_ => {
|
|
1198
|
+
/* catch the error, otherwise it will appear in the console. */
|
|
1199
|
+
ex.data._processing = undefined;
|
|
1200
|
+
}
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
content.ent.push(ex);
|
|
1204
|
+
|
|
1205
|
+
return content;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Wraps drafty document into a simple formatting style.
|
|
1210
|
+
* @memberof Drafty
|
|
1211
|
+
* @static
|
|
1212
|
+
*
|
|
1213
|
+
* @param {Drafty|string} content - document or string to wrap into a style.
|
|
1214
|
+
* @param {string} style - two-letter style to wrap into.
|
|
1215
|
+
* @param {number} at - index where the style starts, default 0.
|
|
1216
|
+
* @param {number} len - length of the form content, default all of it.
|
|
1217
|
+
*
|
|
1218
|
+
* @return {Drafty} updated document.
|
|
1219
|
+
*/
|
|
1220
|
+
Drafty.wrapInto = function(content, style, at, len) {
|
|
1221
|
+
if (typeof content == 'string') {
|
|
1222
|
+
content = {
|
|
1223
|
+
txt: content
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
content.fmt = content.fmt || [];
|
|
1227
|
+
|
|
1228
|
+
content.fmt.push({
|
|
1229
|
+
at: at || 0,
|
|
1230
|
+
len: len || content.txt.length,
|
|
1231
|
+
tp: style,
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
return content;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Wraps content into an interactive form.
|
|
1239
|
+
* @memberof Drafty
|
|
1240
|
+
* @static
|
|
1241
|
+
*
|
|
1242
|
+
* @param {Drafty|string} content - to wrap into a form.
|
|
1243
|
+
* @param {number} at - index where the forms starts.
|
|
1244
|
+
* @param {number} len - length of the form content.
|
|
1245
|
+
*
|
|
1246
|
+
* @return {Drafty} updated document.
|
|
1247
|
+
*/
|
|
1248
|
+
Drafty.wrapAsForm = function(content, at, len) {
|
|
1249
|
+
return Drafty.wrapInto(content, 'FM', at, len);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* Insert clickable button into Drafty document.
|
|
1254
|
+
* @memberof Drafty
|
|
1255
|
+
* @static
|
|
1256
|
+
*
|
|
1257
|
+
* @param {Drafty|string} content - Drafty document to insert button to or a string to be used as button text.
|
|
1258
|
+
* @param {number} at - location where the button is inserted.
|
|
1259
|
+
* @param {number} len - the length of the text to be used as button title.
|
|
1260
|
+
* @param {string} name - the button. Client should return it to the server when the button is clicked.
|
|
1261
|
+
* @param {string} actionType - the type of the button, one of 'url' or 'pub'.
|
|
1262
|
+
* @param {string} actionValue - the value to return on click:
|
|
1263
|
+
* @param {string} refUrl - the URL to go to when the 'url' button is clicked.
|
|
1264
|
+
*
|
|
1265
|
+
* @return {Drafty} updated document.
|
|
1266
|
+
*/
|
|
1267
|
+
Drafty.insertButton = function(content, at, len, name, actionType, actionValue, refUrl) {
|
|
1268
|
+
if (typeof content == 'string') {
|
|
1269
|
+
content = {
|
|
1270
|
+
txt: content
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
if (!content || !content.txt || content.txt.length < at + len) {
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
if (len <= 0 || ['url', 'pub'].indexOf(actionType) == -1) {
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
// Ensure refUrl is a string.
|
|
1282
|
+
if (actionType == 'url' && !refUrl) {
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
refUrl = '' + refUrl;
|
|
1286
|
+
|
|
1287
|
+
content.ent = content.ent || [];
|
|
1288
|
+
content.fmt = content.fmt || [];
|
|
1289
|
+
|
|
1290
|
+
content.fmt.push({
|
|
1291
|
+
at: at | 0,
|
|
1292
|
+
len: len,
|
|
1293
|
+
key: content.ent.length
|
|
1294
|
+
});
|
|
1295
|
+
content.ent.push({
|
|
1296
|
+
tp: 'BN',
|
|
1297
|
+
data: {
|
|
1298
|
+
act: actionType,
|
|
1299
|
+
val: actionValue,
|
|
1300
|
+
ref: refUrl,
|
|
1301
|
+
name: name
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
return content;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* Append clickable button to Drafty document.
|
|
1310
|
+
* @memberof Drafty
|
|
1311
|
+
* @static
|
|
1312
|
+
*
|
|
1313
|
+
* @param {Drafty|string} content - Drafty document to insert button to or a string to be used as button text.
|
|
1314
|
+
* @param {string} title - the text to be used as button title.
|
|
1315
|
+
* @param {string} name - the button. Client should return it to the server when the button is clicked.
|
|
1316
|
+
* @param {string} actionType - the type of the button, one of 'url' or 'pub'.
|
|
1317
|
+
* @param {string} actionValue - the value to return on click:
|
|
1318
|
+
* @param {string} refUrl - the URL to go to when the 'url' button is clicked.
|
|
1319
|
+
*
|
|
1320
|
+
* @return {Drafty} updated document.
|
|
1321
|
+
*/
|
|
1322
|
+
Drafty.appendButton = function(content, title, name, actionType, actionValue, refUrl) {
|
|
1323
|
+
content = content || {
|
|
1324
|
+
txt: ''
|
|
1325
|
+
};
|
|
1326
|
+
const at = content.txt.length;
|
|
1327
|
+
content.txt += title;
|
|
1328
|
+
return Drafty.insertButton(content, at, title.length, name, actionType, actionValue, refUrl);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Attach a generic JS object. The object is attached as a json string.
|
|
1333
|
+
* Intended for representing a form response.
|
|
1334
|
+
*
|
|
1335
|
+
* @memberof Drafty
|
|
1336
|
+
* @static
|
|
1337
|
+
*
|
|
1338
|
+
* @param {Drafty} content - Drafty document to attach file to.
|
|
1339
|
+
* @param {Object} data - data to convert to json string and attach.
|
|
1340
|
+
* @returns {Drafty} the same document as <code>content</code>.
|
|
1341
|
+
*/
|
|
1342
|
+
Drafty.attachJSON = function(content, data) {
|
|
1343
|
+
content = content || {
|
|
1344
|
+
txt: ''
|
|
1345
|
+
};
|
|
1346
|
+
content.ent = content.ent || [];
|
|
1347
|
+
content.fmt = content.fmt || [];
|
|
1348
|
+
|
|
1349
|
+
content.fmt.push({
|
|
1350
|
+
at: -1,
|
|
1351
|
+
len: 0,
|
|
1352
|
+
key: content.ent.length
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
content.ent.push({
|
|
1356
|
+
tp: 'EX',
|
|
1357
|
+
data: {
|
|
1358
|
+
mime: DRAFTY_FR_MIME_TYPE,
|
|
1359
|
+
val: data
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
return content;
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Append line break to a Drafty document.
|
|
1367
|
+
* @memberof Drafty
|
|
1368
|
+
* @static
|
|
1369
|
+
*
|
|
1370
|
+
* @param {Drafty} content - Drafty document to append linebreak to.
|
|
1371
|
+
* @returns {Drafty} the same document as <code>content</code>.
|
|
1372
|
+
*/
|
|
1373
|
+
Drafty.appendLineBreak = function(content) {
|
|
1374
|
+
content = content || {
|
|
1375
|
+
txt: ''
|
|
1376
|
+
};
|
|
1377
|
+
content.fmt = content.fmt || [];
|
|
1378
|
+
content.fmt.push({
|
|
1379
|
+
at: stringToGraphemes(content.txt).length,
|
|
1380
|
+
len: 1,
|
|
1381
|
+
tp: 'BR'
|
|
1382
|
+
});
|
|
1383
|
+
content.txt += ' ';
|
|
1384
|
+
|
|
1385
|
+
return content;
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Given Drafty document, convert it to HTML.
|
|
1389
|
+
* No attempt is made to strip pre-existing html markup.
|
|
1390
|
+
* This is potentially unsafe because <code>content.txt</code> may contain malicious HTML
|
|
1391
|
+
* markup. DO NOT use in production code.
|
|
1392
|
+
*
|
|
1393
|
+
* @memberof Tinode.Drafty
|
|
1394
|
+
* @static
|
|
1395
|
+
*
|
|
1396
|
+
* @param {Drafty} doc - document to convert.
|
|
1397
|
+
*
|
|
1398
|
+
* @returns {string} HTML-representation of content.
|
|
1399
|
+
*/
|
|
1400
|
+
Drafty.UNSAFE_toHTML = function(doc) {
|
|
1401
|
+
const tree = draftyToTree(doc);
|
|
1402
|
+
const htmlFormatter = function(type, data, values) {
|
|
1403
|
+
const tag = DECORATORS[type];
|
|
1404
|
+
let result = values ? values.join('') : '';
|
|
1405
|
+
if (tag) {
|
|
1406
|
+
result = tag.open(data) + result + tag.close(data);
|
|
1407
|
+
}
|
|
1408
|
+
return result;
|
|
1409
|
+
};
|
|
1410
|
+
return treeBottomUp(tree, htmlFormatter, 0);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* Callback for applying custom formatting to a Drafty document.
|
|
1415
|
+
* Called once for each style span.
|
|
1416
|
+
* @memberof Drafty
|
|
1417
|
+
* @static
|
|
1418
|
+
*
|
|
1419
|
+
* @callback Formatter
|
|
1420
|
+
* @param {string} style - style code such as "ST" or "IM".
|
|
1421
|
+
* @param {Object} data - entity's data.
|
|
1422
|
+
* @param {Object} values - possibly styled subspans contained in this style span.
|
|
1423
|
+
* @param {number} index - index of the element guaranteed to be unique.
|
|
1424
|
+
*/
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* Convert Drafty document to a representation suitable for display.
|
|
1428
|
+
* The <code>context</code> may expose a function <code>getFormatter(style)</code>. If it's available
|
|
1429
|
+
* it will call it to obtain a <code>formatter</code> for a subtree of styles under the <code>style</code>.
|
|
1430
|
+
* @memberof Drafty
|
|
1431
|
+
* @static
|
|
1432
|
+
*
|
|
1433
|
+
* @param {Drafty|Object} content - Drafty document to transform.
|
|
1434
|
+
* @param {Formatter} formatter - callback which formats individual elements.
|
|
1435
|
+
* @param {Object} context - context provided to formatter as <code>this</code>.
|
|
1436
|
+
*
|
|
1437
|
+
* @return {Object} transformed object
|
|
1438
|
+
*/
|
|
1439
|
+
Drafty.format = function(original, formatter, context) {
|
|
1440
|
+
return treeBottomUp(draftyToTree(original), formatter, 0, [], context);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/**
|
|
1444
|
+
* Shorten Drafty document making the drafty text no longer than the limit.
|
|
1445
|
+
* @memberof Drafty
|
|
1446
|
+
* @static
|
|
1447
|
+
*
|
|
1448
|
+
* @param {Drafty|string} original - Drafty object to shorten.
|
|
1449
|
+
* @param {number} limit - length in characrets to shorten to.
|
|
1450
|
+
* @param {boolean} light - remove heavy data from entities.
|
|
1451
|
+
* @returns new shortened Drafty object leaving the original intact.
|
|
1452
|
+
*/
|
|
1453
|
+
Drafty.shorten = function(original, limit, light) {
|
|
1454
|
+
let tree = draftyToTree(original);
|
|
1455
|
+
tree = shortenTree(tree, limit, '…');
|
|
1456
|
+
if (tree && light) {
|
|
1457
|
+
tree = lightEntity(tree);
|
|
1458
|
+
}
|
|
1459
|
+
return treeToDrafty({}, tree, []);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* Transform Drafty doc for forwarding: strip leading @mention and any leading line breaks or whitespace.
|
|
1464
|
+
* @memberof Drafty
|
|
1465
|
+
* @static
|
|
1466
|
+
*
|
|
1467
|
+
* @param {Drafty|string} original - Drafty object to shorten.
|
|
1468
|
+
* @returns converted Drafty object leaving the original intact.
|
|
1469
|
+
*/
|
|
1470
|
+
Drafty.forwardedContent = function(original) {
|
|
1471
|
+
let tree = draftyToTree(original);
|
|
1472
|
+
const rmMention = function(node) {
|
|
1473
|
+
if (node.type == 'MN') {
|
|
1474
|
+
if (!node.parent || !node.parent.type) {
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return node;
|
|
1479
|
+
}
|
|
1480
|
+
// Strip leading mention.
|
|
1481
|
+
tree = treeTopDown(tree, rmMention);
|
|
1482
|
+
// Remove leading whitespace.
|
|
1483
|
+
tree = lTrim(tree);
|
|
1484
|
+
// Convert back to Drafty.
|
|
1485
|
+
return treeToDrafty({}, tree, []);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* Prepare Drafty doc for wrapping into QQ as a reply:
|
|
1490
|
+
* - Replace forwarding mention with symbol '➦' and remove data (UID).
|
|
1491
|
+
* - Remove quoted text completely.
|
|
1492
|
+
* - Replace line breaks with spaces.
|
|
1493
|
+
* - Strip entities of heavy content.
|
|
1494
|
+
* - Move attachments to the end of the document.
|
|
1495
|
+
* @memberof Drafty
|
|
1496
|
+
* @static
|
|
1497
|
+
*
|
|
1498
|
+
* @param {Drafty|string} original - Drafty object to shorten.
|
|
1499
|
+
* @param {number} limit - length in characters to shorten to.
|
|
1500
|
+
* @returns converted Drafty object leaving the original intact.
|
|
1501
|
+
*/
|
|
1502
|
+
Drafty.replyContent = function(original, limit) {
|
|
1503
|
+
const convMNnQQnBR = function(node) {
|
|
1504
|
+
if (node.type == 'QQ') {
|
|
1505
|
+
return null;
|
|
1506
|
+
} else if (node.type == 'MN') {
|
|
1507
|
+
if ((!node.parent || !node.parent.type) && (node.text || '').startsWith('➦')) {
|
|
1508
|
+
node.text = '➦';
|
|
1509
|
+
delete node.children;
|
|
1510
|
+
delete node.data;
|
|
1511
|
+
}
|
|
1512
|
+
} else if (node.type == 'BR') {
|
|
1513
|
+
node.text = ' ';
|
|
1514
|
+
delete node.type;
|
|
1515
|
+
delete node.children;
|
|
1516
|
+
}
|
|
1517
|
+
return node;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
let tree = draftyToTree(original);
|
|
1521
|
+
if (!tree) {
|
|
1522
|
+
return original;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// Strip leading mention.
|
|
1526
|
+
tree = treeTopDown(tree, convMNnQQnBR);
|
|
1527
|
+
// Move attachments to the end of the doc.
|
|
1528
|
+
tree = attachmentsToEnd(tree, MAX_PREVIEW_ATTACHMENTS);
|
|
1529
|
+
// Shorten the doc.
|
|
1530
|
+
tree = shortenTree(tree, limit, '…');
|
|
1531
|
+
// Strip heavy elements except IM.data['val'] and VD.data['preview'] (have to keep them to generate previews later).
|
|
1532
|
+
const filter = node => {
|
|
1533
|
+
switch (node.type) {
|
|
1534
|
+
case 'IM':
|
|
1535
|
+
return ['val'];
|
|
1536
|
+
case 'VD':
|
|
1537
|
+
return ['preview'];
|
|
1538
|
+
}
|
|
1539
|
+
return null;
|
|
1540
|
+
};
|
|
1541
|
+
tree = lightEntity(tree, filter);
|
|
1542
|
+
// Convert back to Drafty.
|
|
1543
|
+
return treeToDrafty({}, tree, []);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* Generate drafty preview:
|
|
1549
|
+
* - Shorten the document.
|
|
1550
|
+
* - Strip all heavy entity data leaving just inline styles and entity references.
|
|
1551
|
+
* - Replace line breaks with spaces.
|
|
1552
|
+
* - Replace content of QQ with a space.
|
|
1553
|
+
* - Replace forwarding mention with symbol '➦'.
|
|
1554
|
+
* move all attachments to the end of the document and make them visible.
|
|
1555
|
+
* The <code>context</code> may expose a function <code>getFormatter(style)</code>. If it's available
|
|
1556
|
+
* it will call it to obtain a <code>formatter</code> for a subtree of styles under the <code>style</code>.
|
|
1557
|
+
* @memberof Drafty
|
|
1558
|
+
* @static
|
|
1559
|
+
*
|
|
1560
|
+
* @param {Drafty|string} original - Drafty object to shorten.
|
|
1561
|
+
* @param {number} limit - length in characters to shorten to.
|
|
1562
|
+
* @param {boolean} forwarding - this a forwarding message preview.
|
|
1563
|
+
* @returns new shortened Drafty object leaving the original intact.
|
|
1564
|
+
*/
|
|
1565
|
+
Drafty.preview = function(original, limit, forwarding) {
|
|
1566
|
+
let tree = draftyToTree(original);
|
|
1567
|
+
|
|
1568
|
+
// Move attachments to the end.
|
|
1569
|
+
tree = attachmentsToEnd(tree, MAX_PREVIEW_ATTACHMENTS);
|
|
1570
|
+
|
|
1571
|
+
// Convert leading mention to '➦' and replace QQ and BR with a space ' '.
|
|
1572
|
+
const convMNnQQnBR = function(node) {
|
|
1573
|
+
if (node.type == 'MN') {
|
|
1574
|
+
if ((!node.parent || !node.parent.type) && (node.text || '').startsWith('➦')) {
|
|
1575
|
+
node.text = '➦';
|
|
1576
|
+
delete node.children;
|
|
1577
|
+
}
|
|
1578
|
+
} else if (node.type == 'QQ') {
|
|
1579
|
+
node.text = ' ';
|
|
1580
|
+
delete node.children;
|
|
1581
|
+
} else if (node.type == 'BR') {
|
|
1582
|
+
node.text = ' ';
|
|
1583
|
+
delete node.children;
|
|
1584
|
+
delete node.type;
|
|
1585
|
+
}
|
|
1586
|
+
return node;
|
|
1587
|
+
}
|
|
1588
|
+
tree = treeTopDown(tree, convMNnQQnBR);
|
|
1589
|
+
|
|
1590
|
+
tree = shortenTree(tree, limit, '…');
|
|
1591
|
+
if (forwarding) {
|
|
1592
|
+
// Keep some IM and VD data for preview.
|
|
1593
|
+
const filter = {
|
|
1594
|
+
IM: ['val'],
|
|
1595
|
+
VD: ['preview']
|
|
1596
|
+
};
|
|
1597
|
+
tree = lightEntity(tree, node => {
|
|
1598
|
+
return filter[node.type];
|
|
1599
|
+
});
|
|
1600
|
+
} else {
|
|
1601
|
+
tree = lightEntity(tree);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Convert back to Drafty.
|
|
1605
|
+
return treeToDrafty({}, tree, []);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Given Drafty document, convert it to plain text.
|
|
1610
|
+
* @memberof Drafty
|
|
1611
|
+
* @static
|
|
1612
|
+
*
|
|
1613
|
+
* @param {Drafty} content - document to convert to plain text.
|
|
1614
|
+
* @returns {string} plain-text representation of the drafty document.
|
|
1615
|
+
*/
|
|
1616
|
+
Drafty.toPlainText = function(content) {
|
|
1617
|
+
return typeof content == 'string' ? content : content.txt;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/**
|
|
1621
|
+
* Check if the document has no markup and no entities.
|
|
1622
|
+
* @memberof Drafty
|
|
1623
|
+
* @static
|
|
1624
|
+
*
|
|
1625
|
+
* @param {Drafty} content - content to check for presence of markup.
|
|
1626
|
+
* @returns <code>true</code> is content is plain text, <code>false</code> otherwise.
|
|
1627
|
+
*/
|
|
1628
|
+
Drafty.isPlainText = function(content) {
|
|
1629
|
+
return typeof content == 'string' || !(content.fmt || content.ent);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* Convert document to plain text with markdown. All elements which cannot
|
|
1634
|
+
* be represented in markdown are stripped.
|
|
1635
|
+
* @memberof Drafty
|
|
1636
|
+
* @static
|
|
1637
|
+
*
|
|
1638
|
+
* @param {Drafty} content - document to convert to plain text with markdown.
|
|
1639
|
+
*/
|
|
1640
|
+
Drafty.toMarkdown = function(content) {
|
|
1641
|
+
let tree = draftyToTree(content);
|
|
1642
|
+
const mdFormatter = function(type, _, values) {
|
|
1643
|
+
const def = FORMAT_TAGS[type];
|
|
1644
|
+
let result = (values ? values.join('') : '');
|
|
1645
|
+
if (def) {
|
|
1646
|
+
if (def.isVoid) {
|
|
1647
|
+
result = def.md_tag || '';
|
|
1648
|
+
} else if (def.md_tag) {
|
|
1649
|
+
result = def.md_tag + result + def.md_tag;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
return result;
|
|
1653
|
+
};
|
|
1654
|
+
return treeBottomUp(tree, mdFormatter, 0);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* Checks if the object represets is a valid Drafty document.
|
|
1659
|
+
* @memberof Drafty
|
|
1660
|
+
* @static
|
|
1661
|
+
*
|
|
1662
|
+
* @param {Drafty} content - content to check for validity.
|
|
1663
|
+
* @returns <code>true</code> is content is valid, <code>false</code> otherwise.
|
|
1664
|
+
*/
|
|
1665
|
+
Drafty.isValid = function(content) {
|
|
1666
|
+
if (!content) {
|
|
1667
|
+
return false;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
const {
|
|
1671
|
+
txt,
|
|
1672
|
+
fmt,
|
|
1673
|
+
ent
|
|
1674
|
+
} = content;
|
|
1675
|
+
|
|
1676
|
+
if (!txt && txt !== '' && !fmt && !ent) {
|
|
1677
|
+
return false;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
const txt_type = typeof txt;
|
|
1681
|
+
if (txt_type != 'string' && txt_type != 'undefined' && txt !== null) {
|
|
1682
|
+
return false;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
if (typeof fmt != 'undefined' && !Array.isArray(fmt) && fmt !== null) {
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
if (typeof ent != 'undefined' && !Array.isArray(ent) && ent !== null) {
|
|
1690
|
+
return false;
|
|
1691
|
+
}
|
|
1692
|
+
return true;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
/**
|
|
1696
|
+
* Check if the drafty document has attachments: style EX and outside of normal rendering flow,
|
|
1697
|
+
* i.e. <code>at = -1</code>.
|
|
1698
|
+
* @memberof Drafty
|
|
1699
|
+
* @static
|
|
1700
|
+
*
|
|
1701
|
+
* @param {Drafty} content - document to check for attachments.
|
|
1702
|
+
* @returns <code>true</code> if there are attachments.
|
|
1703
|
+
*/
|
|
1704
|
+
Drafty.hasAttachments = function(content) {
|
|
1705
|
+
if (!Array.isArray(content.fmt)) {
|
|
1706
|
+
return false;
|
|
1707
|
+
}
|
|
1708
|
+
for (let i in content.fmt) {
|
|
1709
|
+
const fmt = content.fmt[i];
|
|
1710
|
+
if (fmt && fmt.at < 0) {
|
|
1711
|
+
const ent = content.ent[fmt.key | 0];
|
|
1712
|
+
return ent && ent.tp == 'EX' && ent.data;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* Callback for enumerating entities in a Drafty document.
|
|
1720
|
+
* Called once for each entity.
|
|
1721
|
+
* @memberof Drafty
|
|
1722
|
+
* @static
|
|
1723
|
+
*
|
|
1724
|
+
* @callback EntityCallback
|
|
1725
|
+
* @param {Object} data entity data.
|
|
1726
|
+
* @param {string} entity type.
|
|
1727
|
+
* @param {number} index entity's index in `content.ent`.
|
|
1728
|
+
*
|
|
1729
|
+
* @return 'true-ish' to stop processing, 'false-ish' otherwise.
|
|
1730
|
+
*/
|
|
1731
|
+
|
|
1732
|
+
/**
|
|
1733
|
+
* Enumerate attachments: style EX and outside of normal rendering flow, i.e. <code>at = -1</code>.
|
|
1734
|
+
* @memberof Drafty
|
|
1735
|
+
* @static
|
|
1736
|
+
*
|
|
1737
|
+
* @param {Drafty} content - document to process for attachments.
|
|
1738
|
+
* @param {EntityCallback} callback - callback to call for each attachment.
|
|
1739
|
+
* @param {Object} context - value of "this" for callback.
|
|
1740
|
+
*/
|
|
1741
|
+
Drafty.attachments = function(content, callback, context) {
|
|
1742
|
+
if (!Array.isArray(content.fmt)) {
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
let count = 0;
|
|
1746
|
+
for (let i in content.fmt) {
|
|
1747
|
+
let fmt = content.fmt[i];
|
|
1748
|
+
if (fmt && fmt.at < 0) {
|
|
1749
|
+
const ent = content.ent[fmt.key | 0];
|
|
1750
|
+
if (ent && ent.tp == 'EX' && ent.data) {
|
|
1751
|
+
if (callback.call(context, ent.data, count++, 'EX')) {
|
|
1752
|
+
break;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* Check if the drafty document has entities.
|
|
1761
|
+
* @memberof Drafty
|
|
1762
|
+
* @static
|
|
1763
|
+
*
|
|
1764
|
+
* @param {Drafty} content - document to check for entities.
|
|
1765
|
+
* @returns <code>true</code> if there are entities.
|
|
1766
|
+
*/
|
|
1767
|
+
Drafty.hasEntities = function(content) {
|
|
1768
|
+
return content.ent && content.ent.length > 0;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
/**
|
|
1772
|
+
* Enumerate entities. Enumeration stops if callback returns 'true'.
|
|
1773
|
+
* @memberof Drafty
|
|
1774
|
+
* @static
|
|
1775
|
+
*
|
|
1776
|
+
* @param {Drafty} content - document with entities to enumerate.
|
|
1777
|
+
* @param {EntityCallback} callback - callback to call for each entity.
|
|
1778
|
+
* @param {Object} context - value of "this" for callback.
|
|
1779
|
+
*
|
|
1780
|
+
*/
|
|
1781
|
+
Drafty.entities = function(content, callback, context) {
|
|
1782
|
+
if (content.ent && content.ent.length > 0) {
|
|
1783
|
+
for (let i in content.ent) {
|
|
1784
|
+
if (content.ent[i]) {
|
|
1785
|
+
if (callback.call(context, content.ent[i].data, i, content.ent[i].tp)) {
|
|
1786
|
+
break;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Callback for enumerating styles (inline formats) in a Drafty document.
|
|
1795
|
+
* Called once for each style.
|
|
1796
|
+
* @memberof Drafty
|
|
1797
|
+
* @static
|
|
1798
|
+
*
|
|
1799
|
+
* @callback StyleCallback
|
|
1800
|
+
* @param {string} tp - format type.
|
|
1801
|
+
* @param {number} at - starting position of the format in text.
|
|
1802
|
+
* @param {number} len - extent of the format in characters.
|
|
1803
|
+
* @param {number} key - index of the entity if format is a reference.
|
|
1804
|
+
* @param {number} index - style's index in `content.fmt`.
|
|
1805
|
+
*
|
|
1806
|
+
* @return 'true-ish' to stop processing, 'false-ish' otherwise.
|
|
1807
|
+
*/
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* Enumerate styles (inline formats). Enumeration stops if callback returns 'true'.
|
|
1811
|
+
* @memberof Drafty
|
|
1812
|
+
* @static
|
|
1813
|
+
*
|
|
1814
|
+
* @param {Drafty} content - document with styles (formats) to enumerate.
|
|
1815
|
+
* @param {StyleCallback} callback - callback to call for each format.
|
|
1816
|
+
* @param {Object} context - value of "this" for callback.
|
|
1817
|
+
*/
|
|
1818
|
+
Drafty.styles = function(content, callback, context) {
|
|
1819
|
+
if (content.fmt && content.fmt.length > 0) {
|
|
1820
|
+
for (let i in content.fmt) {
|
|
1821
|
+
const fmt = content.fmt[i];
|
|
1822
|
+
if (fmt) {
|
|
1823
|
+
if (callback.call(context, fmt.tp, fmt.at, fmt.len, fmt.key, i)) {
|
|
1824
|
+
break;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
/**
|
|
1832
|
+
* Remove unrecognized fields from entity data
|
|
1833
|
+
* @memberof Drafty
|
|
1834
|
+
* @static
|
|
1835
|
+
*
|
|
1836
|
+
* @param {Drafty} content - document with entities to enumerate.
|
|
1837
|
+
* @returns content.
|
|
1838
|
+
*/
|
|
1839
|
+
Drafty.sanitizeEntities = function(content) {
|
|
1840
|
+
if (content && content.ent && content.ent.length > 0) {
|
|
1841
|
+
for (let i in content.ent) {
|
|
1842
|
+
const ent = content.ent[i];
|
|
1843
|
+
if (ent && ent.data) {
|
|
1844
|
+
const data = copyEntData(ent.data);
|
|
1845
|
+
if (data) {
|
|
1846
|
+
content.ent[i].data = data;
|
|
1847
|
+
} else {
|
|
1848
|
+
delete content.ent[i].data;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
return content;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/**
|
|
1857
|
+
* Given the entity, get URL which can be used for downloading
|
|
1858
|
+
* entity data.
|
|
1859
|
+
* @memberof Drafty
|
|
1860
|
+
* @static
|
|
1861
|
+
*
|
|
1862
|
+
* @param {Object} entData - entity.data to get the URl from.
|
|
1863
|
+
* @returns {string} URL to download entity data or <code>null</code>.
|
|
1864
|
+
*/
|
|
1865
|
+
Drafty.getDownloadUrl = function(entData) {
|
|
1866
|
+
let url = null;
|
|
1867
|
+
if (!Drafty.isFormResponseType(entData.mime) && entData.val) {
|
|
1868
|
+
url = base64toObjectUrl(entData.val, entData.mime, Drafty.logger);
|
|
1869
|
+
} else if (typeof entData.ref == 'string') {
|
|
1870
|
+
url = entData.ref;
|
|
1871
|
+
}
|
|
1872
|
+
return url;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
/**
|
|
1876
|
+
* Check if the entity data is not ready for sending, such as being uploaded to the server.
|
|
1877
|
+
* @memberof Drafty
|
|
1878
|
+
* @static
|
|
1879
|
+
*
|
|
1880
|
+
* @param {Object} entity.data to get the URl from.
|
|
1881
|
+
* @returns {boolean} true if upload is in progress, false otherwise.
|
|
1882
|
+
*/
|
|
1883
|
+
Drafty.isProcessing = function(entData) {
|
|
1884
|
+
return !!entData._processing;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
/**
|
|
1888
|
+
* Given the entity, get URL which can be used for previewing
|
|
1889
|
+
* the entity.
|
|
1890
|
+
* @memberof Drafty
|
|
1891
|
+
* @static
|
|
1892
|
+
*
|
|
1893
|
+
* @param {Object} entity.data to get the URl from.
|
|
1894
|
+
*
|
|
1895
|
+
* @returns {string} url for previewing or null if no such url is available.
|
|
1896
|
+
*/
|
|
1897
|
+
Drafty.getPreviewUrl = function(entData) {
|
|
1898
|
+
return entData.val ? base64toObjectUrl(entData.val, entData.mime, Drafty.logger) : null;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
/**
|
|
1902
|
+
* Get approximate size of the entity.
|
|
1903
|
+
* @memberof Drafty
|
|
1904
|
+
* @static
|
|
1905
|
+
*
|
|
1906
|
+
* @param {Object} entData - entity.data to get the size for.
|
|
1907
|
+
* @returns {number} size of entity data in bytes.
|
|
1908
|
+
*/
|
|
1909
|
+
Drafty.getEntitySize = function(entData) {
|
|
1910
|
+
// Either size hint or length of value. The value is base64 encoded,
|
|
1911
|
+
// the actual object size is smaller than the encoded length.
|
|
1912
|
+
return entData.size ? entData.size : entData.val ? (entData.val.length * 0.75) | 0 : 0;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
/**
|
|
1916
|
+
* Get entity mime type.
|
|
1917
|
+
* @memberof Drafty
|
|
1918
|
+
* @static
|
|
1919
|
+
*
|
|
1920
|
+
* @param {Object} entData - entity.data to get the type for.
|
|
1921
|
+
* @returns {string} mime type of entity.
|
|
1922
|
+
*/
|
|
1923
|
+
Drafty.getEntityMimeType = function(entData) {
|
|
1924
|
+
return entData.mime || 'text/plain';
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
/**
|
|
1928
|
+
* Get HTML tag for a given two-letter style name.
|
|
1929
|
+
* @memberof Drafty
|
|
1930
|
+
* @static
|
|
1931
|
+
*
|
|
1932
|
+
* @param {string} style - two-letter style, like ST or LN.
|
|
1933
|
+
*
|
|
1934
|
+
* @returns {string} HTML tag name if style is found, {code: undefined} if style is falsish or not found.
|
|
1935
|
+
*/
|
|
1936
|
+
Drafty.tagName = function(style) {
|
|
1937
|
+
return FORMAT_TAGS[style] && FORMAT_TAGS[style].html_tag;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
/**
|
|
1941
|
+
* For a given data bundle generate an object with HTML attributes,
|
|
1942
|
+
* for instance, given {url: "http://www.example.com/"} return
|
|
1943
|
+
* {href: "http://www.example.com/"}
|
|
1944
|
+
* @memberof Drafty
|
|
1945
|
+
* @static
|
|
1946
|
+
*
|
|
1947
|
+
* @param {string} style - two-letter style to generate attributes for.
|
|
1948
|
+
* @param {Object} data - data bundle to convert to attributes
|
|
1949
|
+
*
|
|
1950
|
+
* @returns {Object} object with HTML attributes.
|
|
1951
|
+
*/
|
|
1952
|
+
Drafty.attrValue = function(style, data) {
|
|
1953
|
+
if (data && DECORATORS[style] && DECORATORS[style].props) {
|
|
1954
|
+
return DECORATORS[style].props(data);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
return undefined;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
/**
|
|
1961
|
+
* Drafty MIME type.
|
|
1962
|
+
* @memberof Drafty
|
|
1963
|
+
* @static
|
|
1964
|
+
*
|
|
1965
|
+
* @returns {string} content-Type "text/x-drafty".
|
|
1966
|
+
*/
|
|
1967
|
+
Drafty.getContentType = function() {
|
|
1968
|
+
return DRAFTY_MIME_TYPE;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
/**
|
|
1972
|
+
* Check if the given mime-type is a MIME type of drafty form response.
|
|
1973
|
+
* @memberof Drafty
|
|
1974
|
+
* @static
|
|
1975
|
+
*
|
|
1976
|
+
* @returns {boolean} <code>true</code> if given mime type is drafty form response, <code>false</code> otherwise.
|
|
1977
|
+
*/
|
|
1978
|
+
Drafty.isFormResponseType = function(mimeType) {
|
|
1979
|
+
return mimeType === DRAFTY_FR_MIME_TYPE ||
|
|
1980
|
+
mimeType === DRAFTY_FR_MIME_TYPE_LEGACY;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// =================
|
|
1984
|
+
// Utility methods.
|
|
1985
|
+
// =================
|
|
1986
|
+
|
|
1987
|
+
// Take a string and defined earlier style spans, re-compose them into a tree where each leaf is
|
|
1988
|
+
// a same-style (including unstyled) string. I.e. 'hello *bold _italic_* and ~more~ world' ->
|
|
1989
|
+
// ('hello ', (b: 'bold ', (i: 'italic')), ' and ', (s: 'more'), ' world');
|
|
1990
|
+
//
|
|
1991
|
+
// This is needed in order to clear markup, i.e. 'hello *world*' -> 'hello world' and convert
|
|
1992
|
+
// ranges from markup-ed offsets to plain text offsets.
|
|
1993
|
+
function chunkify(line, start, end, spans) {
|
|
1994
|
+
const chunks = [];
|
|
1995
|
+
|
|
1996
|
+
if (spans.length == 0) {
|
|
1997
|
+
return [];
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
for (let i in spans) {
|
|
2001
|
+
// Get the next chunk from the queue
|
|
2002
|
+
const span = spans[i];
|
|
2003
|
+
|
|
2004
|
+
// Grab the initial unstyled chunk
|
|
2005
|
+
if (span.at > start) {
|
|
2006
|
+
chunks.push({
|
|
2007
|
+
txt: line.slice(start, span.at)
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// Grab the styled chunk. It may include subchunks.
|
|
2012
|
+
const chunk = {
|
|
2013
|
+
tp: span.tp
|
|
2014
|
+
};
|
|
2015
|
+
const chld = chunkify(line, span.at + 1, span.end, span.children);
|
|
2016
|
+
if (chld.length > 0) {
|
|
2017
|
+
chunk.children = chld;
|
|
2018
|
+
} else {
|
|
2019
|
+
chunk.txt = span.txt;
|
|
2020
|
+
}
|
|
2021
|
+
chunks.push(chunk);
|
|
2022
|
+
start = span.end + 1; // '+1' is to skip the formatting character
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// Grab the remaining unstyled chunk, after the last span
|
|
2026
|
+
if (start < end) {
|
|
2027
|
+
chunks.push({
|
|
2028
|
+
txt: line.slice(start, end)
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
return chunks;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// Detect starts and ends of formatting spans. Unformatted spans are
|
|
2036
|
+
// ignored at this stage.
|
|
2037
|
+
function spannify(original, re_start, re_end, type) {
|
|
2038
|
+
const result = [];
|
|
2039
|
+
let index = 0;
|
|
2040
|
+
let line = original.slice(0); // make a copy;
|
|
2041
|
+
|
|
2042
|
+
while (line.length > 0) {
|
|
2043
|
+
// match[0]; // match, like '*abc*'
|
|
2044
|
+
// match[1]; // match captured in parenthesis, like 'abc'
|
|
2045
|
+
// match['index']; // offset where the match started.
|
|
2046
|
+
|
|
2047
|
+
// Find the opening token.
|
|
2048
|
+
const start = re_start.exec(line);
|
|
2049
|
+
if (start == null) {
|
|
2050
|
+
break;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// Because javascript RegExp does not support lookbehind, the actual offset may not point
|
|
2054
|
+
// at the markup character. Find it in the matched string.
|
|
2055
|
+
let start_offset = start['index'] + start[0].lastIndexOf(start[1]);
|
|
2056
|
+
// Clip the processed part of the string.
|
|
2057
|
+
line = line.slice(start_offset + 1);
|
|
2058
|
+
// start_offset is an offset within the clipped string. Convert to original index.
|
|
2059
|
+
start_offset += index;
|
|
2060
|
+
// Index now point to the beginning of 'line' within the 'original' string.
|
|
2061
|
+
index = start_offset + 1;
|
|
2062
|
+
|
|
2063
|
+
// Find the matching closing token.
|
|
2064
|
+
const end = re_end ? re_end.exec(line) : null;
|
|
2065
|
+
if (end == null) {
|
|
2066
|
+
break;
|
|
2067
|
+
}
|
|
2068
|
+
let end_offset = end['index'] + end[0].indexOf(end[1]);
|
|
2069
|
+
// Clip the processed part of the string.
|
|
2070
|
+
line = line.slice(end_offset + 1);
|
|
2071
|
+
// Update offsets
|
|
2072
|
+
end_offset += index;
|
|
2073
|
+
// Index now points to the beginning of 'line' within the 'original' string.
|
|
2074
|
+
index = end_offset + 1;
|
|
2075
|
+
|
|
2076
|
+
result.push({
|
|
2077
|
+
txt: original.slice(start_offset + 1, end_offset),
|
|
2078
|
+
children: [],
|
|
2079
|
+
at: start_offset,
|
|
2080
|
+
end: end_offset,
|
|
2081
|
+
tp: type
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
return result;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// Convert linear array or spans into a tree representation.
|
|
2089
|
+
// Keep standalone and nested spans, throw away partially overlapping spans.
|
|
2090
|
+
function toSpanTree(spans) {
|
|
2091
|
+
if (spans.length == 0) {
|
|
2092
|
+
return [];
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
const tree = [spans[0]];
|
|
2096
|
+
let last = spans[0];
|
|
2097
|
+
for (let i = 1; i < spans.length; i++) {
|
|
2098
|
+
// Keep spans which start after the end of the previous span or those which
|
|
2099
|
+
// are complete within the previous span.
|
|
2100
|
+
if (spans[i].at > last.end) {
|
|
2101
|
+
// Span is completely outside of the previous span.
|
|
2102
|
+
tree.push(spans[i]);
|
|
2103
|
+
last = spans[i];
|
|
2104
|
+
} else if (spans[i].end <= last.end) {
|
|
2105
|
+
// Span is fully inside of the previous span. Push to subnode.
|
|
2106
|
+
last.children.push(spans[i]);
|
|
2107
|
+
}
|
|
2108
|
+
// Span could partially overlap, ignoring it as invalid.
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// Recursively rearrange the subnodes.
|
|
2112
|
+
for (let i in tree) {
|
|
2113
|
+
tree[i].children = toSpanTree(tree[i].children);
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
return tree;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// Convert drafty document to a tree.
|
|
2120
|
+
function draftyToTree(doc) {
|
|
2121
|
+
if (!doc) {
|
|
2122
|
+
return null;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
doc = (typeof doc == 'string') ? {
|
|
2126
|
+
txt: doc
|
|
2127
|
+
} : doc;
|
|
2128
|
+
let {
|
|
2129
|
+
txt,
|
|
2130
|
+
fmt,
|
|
2131
|
+
ent
|
|
2132
|
+
} = doc;
|
|
2133
|
+
|
|
2134
|
+
txt = txt || '';
|
|
2135
|
+
if (!Array.isArray(ent)) {
|
|
2136
|
+
ent = [];
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
if (!Array.isArray(fmt) || fmt.length == 0) {
|
|
2140
|
+
if (ent.length == 0) {
|
|
2141
|
+
return {
|
|
2142
|
+
text: txt
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// Handle special case when all values in fmt are 0 and fmt therefore is skipped.
|
|
2147
|
+
fmt = [{
|
|
2148
|
+
at: 0,
|
|
2149
|
+
len: 0,
|
|
2150
|
+
key: 0
|
|
2151
|
+
}];
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// Sanitize spans.
|
|
2155
|
+
const spans = [];
|
|
2156
|
+
const attachments = [];
|
|
2157
|
+
fmt.forEach((span) => {
|
|
2158
|
+
if (!span || typeof span != 'object') {
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
if (!['undefined', 'number'].includes(typeof span.at)) {
|
|
2163
|
+
// Present, but non-numeric 'at'.
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
if (!['undefined', 'number'].includes(typeof span.len)) {
|
|
2167
|
+
// Present, but non-numeric 'len'.
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
let at = span.at | 0;
|
|
2171
|
+
let len = span.len | 0;
|
|
2172
|
+
if (len < 0) {
|
|
2173
|
+
// Invalid span length.
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
let key = span.key || 0;
|
|
2178
|
+
if (ent.length > 0 && (typeof key != 'number' || key < 0 || key >= ent.length)) {
|
|
2179
|
+
// Invalid key value.
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
if (at <= -1) {
|
|
2184
|
+
// Attachment. Store attachments separately.
|
|
2185
|
+
attachments.push({
|
|
2186
|
+
start: -1,
|
|
2187
|
+
end: 0,
|
|
2188
|
+
key: key
|
|
2189
|
+
});
|
|
2190
|
+
return;
|
|
2191
|
+
} else if (at + len > stringToGraphemes(txt).length) {
|
|
2192
|
+
// Span is out of bounds.
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
if (!span.tp) {
|
|
2197
|
+
if (ent.length > 0 && (typeof ent[key] == 'object')) {
|
|
2198
|
+
spans.push({
|
|
2199
|
+
start: at,
|
|
2200
|
+
end: at + len,
|
|
2201
|
+
key: key
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
} else {
|
|
2205
|
+
spans.push({
|
|
2206
|
+
type: span.tp,
|
|
2207
|
+
start: at,
|
|
2208
|
+
end: at + len
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
});
|
|
2212
|
+
|
|
2213
|
+
// Sort spans first by start index (asc) then by length (desc), then by weight.
|
|
2214
|
+
spans.sort((a, b) => {
|
|
2215
|
+
let diff = a.start - b.start;
|
|
2216
|
+
if (diff != 0) {
|
|
2217
|
+
return diff;
|
|
2218
|
+
}
|
|
2219
|
+
diff = b.end - a.end;
|
|
2220
|
+
if (diff != 0) {
|
|
2221
|
+
return diff;
|
|
2222
|
+
}
|
|
2223
|
+
return FMT_WEIGHT.indexOf(b.type) - FMT_WEIGHT.indexOf(a.type);
|
|
2224
|
+
});
|
|
2225
|
+
|
|
2226
|
+
// Move attachments to the end of the list.
|
|
2227
|
+
if (attachments.length > 0) {
|
|
2228
|
+
spans.push(...attachments);
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
spans.forEach((span) => {
|
|
2232
|
+
if (ent.length > 0 && !span.type && ent[span.key] && typeof ent[span.key] == 'object') {
|
|
2233
|
+
span.type = ent[span.key].tp;
|
|
2234
|
+
span.data = ent[span.key].data;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
// Is type still undefined? Hide the invalid element!
|
|
2238
|
+
if (!span.type) {
|
|
2239
|
+
span.type = 'HD';
|
|
2240
|
+
}
|
|
2241
|
+
});
|
|
2242
|
+
|
|
2243
|
+
const graphemes = stringToGraphemes(txt);
|
|
2244
|
+
let tree = spansToTree({}, graphemes, 0, graphemes.length, spans);
|
|
2245
|
+
|
|
2246
|
+
// Flatten tree nodes.
|
|
2247
|
+
const flatten = function(node) {
|
|
2248
|
+
if (Array.isArray(node.children) && node.children.length == 1) {
|
|
2249
|
+
// Unwrap.
|
|
2250
|
+
const child = node.children[0];
|
|
2251
|
+
if (!node.type) {
|
|
2252
|
+
const parent = node.parent;
|
|
2253
|
+
node = child;
|
|
2254
|
+
node.parent = parent;
|
|
2255
|
+
} else if (!child.type && !child.children) {
|
|
2256
|
+
node.text = child.text;
|
|
2257
|
+
delete node.children;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
return node;
|
|
2261
|
+
}
|
|
2262
|
+
tree = treeTopDown(tree, flatten);
|
|
2263
|
+
|
|
2264
|
+
return tree;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
// Add tree node to a parent tree.
|
|
2268
|
+
function addNode(parent, n) {
|
|
2269
|
+
if (!n) {
|
|
2270
|
+
return parent;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
if (!parent.children) {
|
|
2274
|
+
parent.children = [];
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
// If text is present, move it to a subnode.
|
|
2278
|
+
if (parent.text) {
|
|
2279
|
+
parent.children.push({
|
|
2280
|
+
text: parent.text,
|
|
2281
|
+
parent: parent
|
|
2282
|
+
});
|
|
2283
|
+
delete parent.text;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
n.parent = parent;
|
|
2287
|
+
parent.children.push(n);
|
|
2288
|
+
|
|
2289
|
+
return parent;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// Returns a tree of nodes.
|
|
2293
|
+
function spansToTree(parent, graphemes, start, end, spans) {
|
|
2294
|
+
if (!spans || spans.length == 0) {
|
|
2295
|
+
if (start < end) {
|
|
2296
|
+
addNode(parent, {
|
|
2297
|
+
text: graphemes.slice(start, end)
|
|
2298
|
+
.map(segment => segment.segment)
|
|
2299
|
+
.join('')
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
return parent;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// Process subspans.
|
|
2306
|
+
for (let i = 0; i < spans.length; i++) {
|
|
2307
|
+
const span = spans[i];
|
|
2308
|
+
if (span.start < 0 && span.type == 'EX') {
|
|
2309
|
+
addNode(parent, {
|
|
2310
|
+
type: span.type,
|
|
2311
|
+
data: span.data,
|
|
2312
|
+
key: span.key,
|
|
2313
|
+
att: true
|
|
2314
|
+
});
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
// Add un-styled range before the styled span starts.
|
|
2319
|
+
if (start < span.start) {
|
|
2320
|
+
addNode(parent, {
|
|
2321
|
+
text: graphemes.slice(start, span.start)
|
|
2322
|
+
.map(segment => segment.segment)
|
|
2323
|
+
.join('')
|
|
2324
|
+
});
|
|
2325
|
+
start = span.start;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// Get all spans which are within the current span.
|
|
2329
|
+
const subspans = [];
|
|
2330
|
+
while (i < spans.length - 1) {
|
|
2331
|
+
const inner = spans[i + 1];
|
|
2332
|
+
if (inner.start < 0) {
|
|
2333
|
+
// Attachments are in the end. Stop.
|
|
2334
|
+
break;
|
|
2335
|
+
} else if (inner.start < span.end) {
|
|
2336
|
+
if (inner.end <= span.end) {
|
|
2337
|
+
const tag = FORMAT_TAGS[inner.tp] || {};
|
|
2338
|
+
if (inner.start < inner.end || tag.isVoid) {
|
|
2339
|
+
// Valid subspan: completely within the current span and
|
|
2340
|
+
// either non-zero length or zero length is acceptable.
|
|
2341
|
+
subspans.push(inner);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
i++;
|
|
2345
|
+
// Overlapping subspans are ignored.
|
|
2346
|
+
} else {
|
|
2347
|
+
// Past the end of the current span. Stop.
|
|
2348
|
+
break;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
addNode(parent, spansToTree({
|
|
2353
|
+
type: span.type,
|
|
2354
|
+
data: span.data,
|
|
2355
|
+
key: span.key
|
|
2356
|
+
}, graphemes, start, span.end, subspans));
|
|
2357
|
+
start = span.end;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
// Add the last unformatted range.
|
|
2361
|
+
if (start < end) {
|
|
2362
|
+
addNode(parent, {
|
|
2363
|
+
text: graphemes
|
|
2364
|
+
.slice(start, end)
|
|
2365
|
+
.map((segment) => segment.segment)
|
|
2366
|
+
.join('')
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
return parent;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// Append a tree to a Drafty doc.
|
|
2374
|
+
function treeToDrafty(doc, tree, keymap) {
|
|
2375
|
+
if (!tree) {
|
|
2376
|
+
return doc;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
doc.txt = doc.txt || '';
|
|
2380
|
+
|
|
2381
|
+
// Checkpoint to measure length of the current tree node.
|
|
2382
|
+
const start = stringToGraphemes(doc.txt).length;
|
|
2383
|
+
|
|
2384
|
+
if (tree.text) {
|
|
2385
|
+
doc.txt += tree.text;
|
|
2386
|
+
} else if (Array.isArray(tree.children)) {
|
|
2387
|
+
tree.children.forEach((c) => {
|
|
2388
|
+
treeToDrafty(doc, c, keymap);
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
if (tree.type) {
|
|
2393
|
+
const len = stringToGraphemes(doc.txt).length - start;
|
|
2394
|
+
doc.fmt = doc.fmt || [];
|
|
2395
|
+
if (Object.keys(tree.data || {}).length > 0) {
|
|
2396
|
+
doc.ent = doc.ent || [];
|
|
2397
|
+
const newKey = (typeof keymap[tree.key] == 'undefined') ? doc.ent.length : keymap[tree.key];
|
|
2398
|
+
keymap[tree.key] = newKey;
|
|
2399
|
+
doc.ent[newKey] = {
|
|
2400
|
+
tp: tree.type,
|
|
2401
|
+
data: tree.data
|
|
2402
|
+
};
|
|
2403
|
+
if (tree.att) {
|
|
2404
|
+
// Attachment.
|
|
2405
|
+
doc.fmt.push({
|
|
2406
|
+
at: -1,
|
|
2407
|
+
len: 0,
|
|
2408
|
+
key: newKey
|
|
2409
|
+
});
|
|
2410
|
+
} else {
|
|
2411
|
+
doc.fmt.push({
|
|
2412
|
+
at: start,
|
|
2413
|
+
len: len,
|
|
2414
|
+
key: newKey
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
} else {
|
|
2418
|
+
doc.fmt.push({
|
|
2419
|
+
tp: tree.type,
|
|
2420
|
+
at: start,
|
|
2421
|
+
len: len
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
return doc;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
// Traverse the tree top down transforming the nodes: apply transformer to every tree node.
|
|
2429
|
+
function treeTopDown(src, transformer, context) {
|
|
2430
|
+
if (!src) {
|
|
2431
|
+
return null;
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
let dst = transformer.call(context, src);
|
|
2435
|
+
if (!dst || !dst.children) {
|
|
2436
|
+
return dst;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
const children = [];
|
|
2440
|
+
for (let i in dst.children) {
|
|
2441
|
+
let n = dst.children[i];
|
|
2442
|
+
if (n) {
|
|
2443
|
+
n = treeTopDown(n, transformer, context);
|
|
2444
|
+
if (n) {
|
|
2445
|
+
children.push(n);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
if (children.length == 0) {
|
|
2451
|
+
dst.children = null;
|
|
2452
|
+
} else {
|
|
2453
|
+
dst.children = children;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
return dst;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// Traverse the tree bottom-up: apply formatter to every node.
|
|
2460
|
+
// The formatter must maintain its state through context.
|
|
2461
|
+
function treeBottomUp(src, formatter, index, stack, context) {
|
|
2462
|
+
if (!src) {
|
|
2463
|
+
return null;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
if (stack && src.type) {
|
|
2467
|
+
stack.push(src.type);
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
let values = [];
|
|
2471
|
+
for (let i in src.children) {
|
|
2472
|
+
const n = treeBottomUp(src.children[i], formatter, i, stack, context);
|
|
2473
|
+
if (n) {
|
|
2474
|
+
values.push(n);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
if (values.length == 0) {
|
|
2478
|
+
if (src.text) {
|
|
2479
|
+
values = [src.text];
|
|
2480
|
+
} else {
|
|
2481
|
+
values = null;
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
if (stack && src.type) {
|
|
2486
|
+
stack.pop();
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
return formatter.call(context, src.type, src.data, values, index, stack);
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// Clip tree to the provided limit.
|
|
2493
|
+
function shortenTree(tree, limit, tail) {
|
|
2494
|
+
if (!tree) {
|
|
2495
|
+
return null;
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
if (tail) {
|
|
2499
|
+
limit -= tail.length;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
const shortener = function(node) {
|
|
2503
|
+
if (limit <= -1) {
|
|
2504
|
+
// Limit -1 means the doc was already clipped.
|
|
2505
|
+
return null;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
if (node.att) {
|
|
2509
|
+
// Attachments are unchanged.
|
|
2510
|
+
return node;
|
|
2511
|
+
}
|
|
2512
|
+
if (limit == 0) {
|
|
2513
|
+
node.text = tail;
|
|
2514
|
+
limit = -1;
|
|
2515
|
+
} else if (node.text) {
|
|
2516
|
+
const graphemes = stringToGraphemes(node.text);
|
|
2517
|
+
if (graphemes.length > limit) {
|
|
2518
|
+
node.text = graphemes
|
|
2519
|
+
.slice(0, limit)
|
|
2520
|
+
.map((segment) => segment.segment)
|
|
2521
|
+
.join('') + tail;
|
|
2522
|
+
limit = -1;
|
|
2523
|
+
} else {
|
|
2524
|
+
limit -= graphemes.length;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
return node;
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
return treeTopDown(tree, shortener);
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// Strip heavy entities from a tree.
|
|
2534
|
+
function lightEntity(tree, allow) {
|
|
2535
|
+
const lightCopy = node => {
|
|
2536
|
+
const data = copyEntData(node.data, true, allow ? allow(node) : null);
|
|
2537
|
+
if (data) {
|
|
2538
|
+
node.data = data;
|
|
2539
|
+
} else {
|
|
2540
|
+
delete node.data;
|
|
2541
|
+
}
|
|
2542
|
+
return node;
|
|
2543
|
+
}
|
|
2544
|
+
return treeTopDown(tree, lightCopy);
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// Remove spaces and breaks on the left.
|
|
2548
|
+
function lTrim(tree) {
|
|
2549
|
+
if (tree.type == 'BR') {
|
|
2550
|
+
tree = null;
|
|
2551
|
+
} else if (tree.text) {
|
|
2552
|
+
if (!tree.type) {
|
|
2553
|
+
tree.text = tree.text.trimStart();
|
|
2554
|
+
if (!tree.text) {
|
|
2555
|
+
tree = null;
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
} else if (!tree.type && tree.children && tree.children.length > 0) {
|
|
2559
|
+
const c = lTrim(tree.children[0]);
|
|
2560
|
+
if (c) {
|
|
2561
|
+
tree.children[0] = c;
|
|
2562
|
+
} else {
|
|
2563
|
+
tree.children.shift();
|
|
2564
|
+
if (!tree.type && tree.children.length == 0) {
|
|
2565
|
+
tree = null;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
return tree;
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
// Move attachments to the end. Attachments must be at the top level, no need to traverse the tree.
|
|
2573
|
+
function attachmentsToEnd(tree, limit) {
|
|
2574
|
+
if (!tree) {
|
|
2575
|
+
return null;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
if (tree.att) {
|
|
2579
|
+
tree.text = ' ';
|
|
2580
|
+
delete tree.att;
|
|
2581
|
+
delete tree.children;
|
|
2582
|
+
} else if (tree.children) {
|
|
2583
|
+
const attachments = [];
|
|
2584
|
+
const children = [];
|
|
2585
|
+
for (let i in tree.children) {
|
|
2586
|
+
const c = tree.children[i];
|
|
2587
|
+
if (c.att) {
|
|
2588
|
+
if (attachments.length == limit) {
|
|
2589
|
+
// Too many attachments to preview;
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
if (Drafty.isFormResponseType(c.data['mime'])) {
|
|
2593
|
+
// Form response attachments are not shown in preview.
|
|
2594
|
+
continue;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
delete c.att;
|
|
2598
|
+
delete c.children;
|
|
2599
|
+
c.text = ' ';
|
|
2600
|
+
attachments.push(c);
|
|
2601
|
+
} else {
|
|
2602
|
+
children.push(c);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
tree.children = children.concat(attachments);
|
|
2606
|
+
}
|
|
2607
|
+
return tree;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
// Get a list of entities from a text.
|
|
2611
|
+
function extractEntities(line) {
|
|
2612
|
+
let match;
|
|
2613
|
+
let extracted = [];
|
|
2614
|
+
ENTITY_TYPES.forEach((entity) => {
|
|
2615
|
+
while ((match = entity.re.exec(line)) !== null) {
|
|
2616
|
+
extracted.push({
|
|
2617
|
+
offset: match['index'],
|
|
2618
|
+
len: match[0].length,
|
|
2619
|
+
unique: match[0],
|
|
2620
|
+
data: entity.pack(match[0]),
|
|
2621
|
+
type: entity.name
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2626
|
+
if (extracted.length == 0) {
|
|
2627
|
+
return extracted;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
// Remove entities detected inside other entities, like #hashtag in a URL.
|
|
2631
|
+
extracted.sort((a, b) => {
|
|
2632
|
+
return a.offset - b.offset;
|
|
2633
|
+
});
|
|
2634
|
+
|
|
2635
|
+
let idx = -1;
|
|
2636
|
+
extracted = extracted.filter((el) => {
|
|
2637
|
+
const result = (el.offset > idx);
|
|
2638
|
+
idx = el.offset + el.len;
|
|
2639
|
+
return result;
|
|
2640
|
+
});
|
|
2641
|
+
|
|
2642
|
+
return extracted;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
// Convert the chunks into format suitable for serialization.
|
|
2646
|
+
function draftify(chunks, startAt) {
|
|
2647
|
+
let plain = '';
|
|
2648
|
+
let ranges = [];
|
|
2649
|
+
for (let i in chunks) {
|
|
2650
|
+
const chunk = chunks[i];
|
|
2651
|
+
if (!chunk.txt) {
|
|
2652
|
+
const drafty = draftify(chunk.children, plain.length + startAt);
|
|
2653
|
+
chunk.txt = drafty.txt;
|
|
2654
|
+
ranges = ranges.concat(drafty.fmt);
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
if (chunk.tp) {
|
|
2658
|
+
ranges.push({
|
|
2659
|
+
at: plain.length + startAt,
|
|
2660
|
+
len: chunk.txt.length,
|
|
2661
|
+
tp: chunk.tp
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
plain += chunk.txt;
|
|
2666
|
+
}
|
|
2667
|
+
return {
|
|
2668
|
+
txt: plain,
|
|
2669
|
+
fmt: ranges
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
// Create a copy of entity data with (light=false) or without (light=true) the large payload.
|
|
2674
|
+
// The array 'allow' contains a list of fields exempt from stripping.
|
|
2675
|
+
function copyEntData(data, light, allow) {
|
|
2676
|
+
if (data && Object.entries(data).length > 0) {
|
|
2677
|
+
allow = allow || [];
|
|
2678
|
+
const dc = {};
|
|
2679
|
+
ALLOWED_ENT_FIELDS.forEach(key => {
|
|
2680
|
+
if (data[key]) {
|
|
2681
|
+
if (light && !allow.includes(key) &&
|
|
2682
|
+
(typeof data[key] == 'string' || Array.isArray(data[key])) &&
|
|
2683
|
+
data[key].length > MAX_PREVIEW_DATA_SIZE) {
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
if (typeof data[key] == 'object') {
|
|
2687
|
+
return;
|
|
2688
|
+
}
|
|
2689
|
+
dc[key] = data[key];
|
|
2690
|
+
}
|
|
2691
|
+
});
|
|
2692
|
+
|
|
2693
|
+
if (Object.entries(dc).length != 0) {
|
|
2694
|
+
return dc;
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
return null;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
// Returns true if object is empty, if undefined returns true
|
|
2701
|
+
function isEmptyObject(obj) {
|
|
2702
|
+
return Object.keys(obj ?? {}).length == 0;
|
|
2703
|
+
};
|
|
2704
|
+
|
|
2705
|
+
|
|
2706
|
+
// Returns an array (of length equal to the length of the original string) such that each index
|
|
2707
|
+
// denotes the position of char in string in a grapheme array (created from that string)
|
|
2708
|
+
// Eg: string: "Hi👋🏼Hi" -> [0,1,2,2,2,2,3,4]
|
|
2709
|
+
function graphemeIndices(graphemes) {
|
|
2710
|
+
const result = [];
|
|
2711
|
+
let graphemeIndex = 0;
|
|
2712
|
+
let charIndex = 0;
|
|
2713
|
+
|
|
2714
|
+
// Iterate over the grapheme clusters.
|
|
2715
|
+
for (const {
|
|
2716
|
+
segment
|
|
2717
|
+
}
|
|
2718
|
+
of graphemes) {
|
|
2719
|
+
// Map the character indices to the grapheme index.
|
|
2720
|
+
for (let i = 0; i < segment.length; i++) {
|
|
2721
|
+
result[charIndex + i] = graphemeIndex;
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
// Increment the character index by the length of the grapheme cluster.
|
|
2725
|
+
charIndex += segment.length;
|
|
2726
|
+
|
|
2727
|
+
// Increment the grapheme index.
|
|
2728
|
+
graphemeIndex++;
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
return result;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
// Convert fmt.at and fmt.len from character-expressed index and length to grapheme-expressed
|
|
2735
|
+
// index and length.
|
|
2736
|
+
function toGraphemeValues(fmt, segments, txt) {
|
|
2737
|
+
segments = segments ?? segmenter.segment(txt);
|
|
2738
|
+
|
|
2739
|
+
const indices = graphemeIndices(segments);
|
|
2740
|
+
|
|
2741
|
+
const correctAt = indices[fmt.at];
|
|
2742
|
+
const correctLen = fmt.at + fmt.len <= txt.length ?
|
|
2743
|
+
indices[fmt.at + fmt.len - 1] - correctAt : fmt.len;
|
|
2744
|
+
|
|
2745
|
+
return {
|
|
2746
|
+
at: correctAt,
|
|
2747
|
+
len: correctLen + 1
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
// Convert string to graphme cluster array.
|
|
2752
|
+
function stringToGraphemes(str) {
|
|
2753
|
+
return Array.from(segmenter.segment(str));
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
if (typeof module != 'undefined') {
|
|
2757
|
+
module.exports = Drafty;
|
|
2758
|
+
}
|