Fossil SCM

chat: ported in the hyperlink and @username parser from the older chat.tcl script. This is an intermediary workaround until we decide how/whether to do server-side markup handling.

stephan 2020-12-24 07:19 UTC trunk
Commit c5095283fb44031f95972dcc61955c766d2644e47298c145855dd1972331729b
1 file changed +79 -4
+79 -4
--- src/chat.js
+++ src/chat.js
@@ -144,12 +144,11 @@
144144
file selection element. */
145145
document.onpaste = function(event){
146146
const items = event.clipboardData.items,
147147
item = items[0];
148148
if(!item || !item.type) return;
149
- //console.debug("pasted item =",item);
150
- if('file'===item.kind){
149
+ else if('file'===item.kind){
151150
updateDropZoneContent(false/*clear prev state*/);
152151
updateDropZoneContent(items[0].getAsFile());
153152
}else if(false && 'string'===item.kind){
154153
/* ----^^^^^ disabled for now: the intent here is that if
155154
form.msg is not active, populate it with this text, but
@@ -276,10 +275,87 @@
276275
const pRect = f.popup.e.getBoundingClientRect();
277276
x -= pRect.width/3*2;
278277
}
279278
f.popup.show(x, y);
280279
};
280
+
281
+ /**
282
+ Parses the given chat message string for hyperlinks and @NAME
283
+ references, replacing them with markup, then stuffs the parsed
284
+ content into the given target DOM element.
285
+
286
+ This is an intermediary step until we get some basic markup
287
+ support coming from the server side.
288
+ */
289
+ const messageToDOM = function f(str, tgtElem){
290
+ "use strict";
291
+ if(!f.rxUrl){
292
+ f.rxUrl = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim;
293
+ f.rxAt = /@\w+/gmi;
294
+ f.rxNS = /\S/;
295
+ f.ce = (T)=>document.createElement(T);
296
+ f.ct = (T)=>document.createTextNode(T);
297
+ f.replaceUrls = function ff(sub, offset, whole){
298
+ if(offset > ff.prevStart){
299
+ f.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, offset-1)+' ');
300
+ }
301
+ const a = f.ce('a');
302
+ a.setAttribute('href',sub);
303
+ a.setAttribute('target','_blank');
304
+ a.appendChild(f.ct(sub));
305
+ f.accum.push(a);
306
+ ff.prevStart = offset + sub.length + 1;
307
+ };
308
+ f.replaceAtName = function ff(sub, offset,whole){
309
+ if(offset > ff.prevStart){
310
+ ff.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, offset-1)+' ');
311
+ }else if(offset && f.rxNS.test(whole[offset-1])){
312
+ // Sigh: https://stackoverflow.com/questions/52655367
313
+ ff.accum.push(sub);
314
+ return;
315
+ }
316
+ const e = f.ce('span');
317
+ e.classList.add('at-name');
318
+ e.appendChild(f.ct(sub));
319
+ ff.accum.push(e);
320
+ ff.prevStart = offset + sub.length + 1;
321
+ };
322
+ }
323
+ f.accum = []; // accumulate strings and DOM elements here.
324
+ f.rxUrl.lastIndex = f.replaceUrls.prevStart = 0; // reset regex cursor
325
+ str.replace(f.rxUrl, f.replaceUrls);
326
+ // Push remaining non-URL part of the string to the queue...
327
+ if(f.replaceUrls.prevStart < str.length){
328
+ f.accum.push((f.replaceUrls.prevStart?' ':'')+str.substring(f.replaceUrls.prevStart));
329
+ }
330
+ // Pass 2: process @NAME references...
331
+ // TODO: only match NAME if it's the name of a currently participating
332
+ // user. Add a second class if NAME == current user, and style that one
333
+ // differently so that people can more easily see when they're spoken to.
334
+ const accum2 = f.replaceAtName.accum = [];
335
+ f.accum.forEach(function(v){
336
+ if('string'===typeof v){
337
+ f.rxAt.lastIndex = f.replaceAtName.prevStart = 0;
338
+ v.replace(f.rxAt, f.replaceAtName);
339
+ if(f.replaceAtName.prevStart < v.length){
340
+ accum2.push((f.replaceAtName.prevStart?' ':'')+v.substring(f.replaceAtName.prevStart));
341
+ }
342
+ }else{
343
+ accum2.push(v);
344
+ }
345
+ });
346
+ delete f.accum;
347
+ const theTgt = tgtElem || f.ce('div');
348
+ const strings = [];
349
+ accum2.forEach(function(e){
350
+ if('string'===typeof e) strings.push(e);
351
+ else strings.push(e.outerHTML);
352
+ });
353
+ D.parseHtml(theTgt, strings.join(''));
354
+ return theTgt;
355
+ }/*end messageToDOM()*/;
356
+
281357
/** Callback for poll() to inject new content into the page. */
282358
function newcontent(jx){
283359
var i;
284360
for(i=0; i<jx.msgs.length; ++i){
285361
const m = jx.msgs[i];
@@ -332,12 +408,11 @@
332408
const br = D.br();
333409
br.style.clear = "both";
334410
eContent.appendChild(br);
335411
}
336412
if(m.xmsg){
337
- try{D.moveChildrenTo(eContent, D.parseHtml(m.xmsg))}
338
- catch(e){console.error(e)}
413
+ messageToDOM(m.xmsg, eContent);
339414
}
340415
eContent.classList.add('chat-message');
341416
}
342417
}
343418
async function poll(){
344419
--- src/chat.js
+++ src/chat.js
@@ -144,12 +144,11 @@
144 file selection element. */
145 document.onpaste = function(event){
146 const items = event.clipboardData.items,
147 item = items[0];
148 if(!item || !item.type) return;
149 //console.debug("pasted item =",item);
150 if('file'===item.kind){
151 updateDropZoneContent(false/*clear prev state*/);
152 updateDropZoneContent(items[0].getAsFile());
153 }else if(false && 'string'===item.kind){
154 /* ----^^^^^ disabled for now: the intent here is that if
155 form.msg is not active, populate it with this text, but
@@ -276,10 +275,87 @@
276 const pRect = f.popup.e.getBoundingClientRect();
277 x -= pRect.width/3*2;
278 }
279 f.popup.show(x, y);
280 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281 /** Callback for poll() to inject new content into the page. */
282 function newcontent(jx){
283 var i;
284 for(i=0; i<jx.msgs.length; ++i){
285 const m = jx.msgs[i];
@@ -332,12 +408,11 @@
332 const br = D.br();
333 br.style.clear = "both";
334 eContent.appendChild(br);
335 }
336 if(m.xmsg){
337 try{D.moveChildrenTo(eContent, D.parseHtml(m.xmsg))}
338 catch(e){console.error(e)}
339 }
340 eContent.classList.add('chat-message');
341 }
342 }
343 async function poll(){
344
--- src/chat.js
+++ src/chat.js
@@ -144,12 +144,11 @@
144 file selection element. */
145 document.onpaste = function(event){
146 const items = event.clipboardData.items,
147 item = items[0];
148 if(!item || !item.type) return;
149 else if('file'===item.kind){
 
150 updateDropZoneContent(false/*clear prev state*/);
151 updateDropZoneContent(items[0].getAsFile());
152 }else if(false && 'string'===item.kind){
153 /* ----^^^^^ disabled for now: the intent here is that if
154 form.msg is not active, populate it with this text, but
@@ -276,10 +275,87 @@
275 const pRect = f.popup.e.getBoundingClientRect();
276 x -= pRect.width/3*2;
277 }
278 f.popup.show(x, y);
279 };
280
281 /**
282 Parses the given chat message string for hyperlinks and @NAME
283 references, replacing them with markup, then stuffs the parsed
284 content into the given target DOM element.
285
286 This is an intermediary step until we get some basic markup
287 support coming from the server side.
288 */
289 const messageToDOM = function f(str, tgtElem){
290 "use strict";
291 if(!f.rxUrl){
292 f.rxUrl = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim;
293 f.rxAt = /@\w+/gmi;
294 f.rxNS = /\S/;
295 f.ce = (T)=>document.createElement(T);
296 f.ct = (T)=>document.createTextNode(T);
297 f.replaceUrls = function ff(sub, offset, whole){
298 if(offset > ff.prevStart){
299 f.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, offset-1)+' ');
300 }
301 const a = f.ce('a');
302 a.setAttribute('href',sub);
303 a.setAttribute('target','_blank');
304 a.appendChild(f.ct(sub));
305 f.accum.push(a);
306 ff.prevStart = offset + sub.length + 1;
307 };
308 f.replaceAtName = function ff(sub, offset,whole){
309 if(offset > ff.prevStart){
310 ff.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, offset-1)+' ');
311 }else if(offset && f.rxNS.test(whole[offset-1])){
312 // Sigh: https://stackoverflow.com/questions/52655367
313 ff.accum.push(sub);
314 return;
315 }
316 const e = f.ce('span');
317 e.classList.add('at-name');
318 e.appendChild(f.ct(sub));
319 ff.accum.push(e);
320 ff.prevStart = offset + sub.length + 1;
321 };
322 }
323 f.accum = []; // accumulate strings and DOM elements here.
324 f.rxUrl.lastIndex = f.replaceUrls.prevStart = 0; // reset regex cursor
325 str.replace(f.rxUrl, f.replaceUrls);
326 // Push remaining non-URL part of the string to the queue...
327 if(f.replaceUrls.prevStart < str.length){
328 f.accum.push((f.replaceUrls.prevStart?' ':'')+str.substring(f.replaceUrls.prevStart));
329 }
330 // Pass 2: process @NAME references...
331 // TODO: only match NAME if it's the name of a currently participating
332 // user. Add a second class if NAME == current user, and style that one
333 // differently so that people can more easily see when they're spoken to.
334 const accum2 = f.replaceAtName.accum = [];
335 f.accum.forEach(function(v){
336 if('string'===typeof v){
337 f.rxAt.lastIndex = f.replaceAtName.prevStart = 0;
338 v.replace(f.rxAt, f.replaceAtName);
339 if(f.replaceAtName.prevStart < v.length){
340 accum2.push((f.replaceAtName.prevStart?' ':'')+v.substring(f.replaceAtName.prevStart));
341 }
342 }else{
343 accum2.push(v);
344 }
345 });
346 delete f.accum;
347 const theTgt = tgtElem || f.ce('div');
348 const strings = [];
349 accum2.forEach(function(e){
350 if('string'===typeof e) strings.push(e);
351 else strings.push(e.outerHTML);
352 });
353 D.parseHtml(theTgt, strings.join(''));
354 return theTgt;
355 }/*end messageToDOM()*/;
356
357 /** Callback for poll() to inject new content into the page. */
358 function newcontent(jx){
359 var i;
360 for(i=0; i<jx.msgs.length; ++i){
361 const m = jx.msgs[i];
@@ -332,12 +408,11 @@
408 const br = D.br();
409 br.style.clear = "both";
410 eContent.appendChild(br);
411 }
412 if(m.xmsg){
413 messageToDOM(m.xmsg, eContent);
 
414 }
415 eContent.classList.add('chat-message');
416 }
417 }
418 async function poll(){
419

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button