No problem. I give you CharNotes: on('ready',function(){
'use strict';
const decodeUnicode = (str) => str.replace(/%u[0-9a-fA-F]{2,4}/g,(m)=>String.fromCharCode(parseInt(m.slice(2),16)));
on('chat:message',function(msg){
if('api' === msg.type && msg.content.match( /^!c(gmnotes|bio)/ ) && playerIsGM(msg.playerid) ){
let match=msg.content.match( /^!c(gmnotes|bio)(?:-(.*))?$/ ),
where = match[1],
regex;
if(match && match[2] ){
regex = new RegExp(`^${ match[2] }`,'i');
}
_.chain(msg.selected)
.map( s => getObj('graphic',s._id))
.reject(_.isUndefined)
.map( t => getObj('character', t.get('represents')))
.reject(_.isUndefined)
.each( c => c.get(where, (val) => {
if(null !== val){
if(regex){
let lines=_.filter(
decodeUnicode(val).split(/(?:[\n\r]+|<br\/?>)/),
(l)=>regex.test(l .replace(/<[^>]*> /g,''))
).join('\r');
sendChat(c.get('name'),`/w gm ` + lines);
} else {
sendChat(c.get('name'),'/w gm &{template:5e-shaped}{{title=' + c.get('name') +'}} {{text='+ decodeUnicode(val) +'}}');
}
}
}));
}
});
}); Works just like GMNote, but with these commands: !cbio
!cgmnotes
!cbio-Some Line Beginning
!cgmnotes-Some Line Beginning Note that I changed it to gmnotes with an s. If that's too annoying, I did that so I could just reuse the part of the command that tells which field to look at, but I can append an s in the code if that breaks your muscle memory. I bolded all the places I changed above, here's a breakdown: I changed the Regular Expression to match the command and the related regular expression to grab the part to match (which now matches the field, too): /^!c(gmnotes|bio)/
/^!c(gmnotes|bio)(?:-(.*))?$/ ^ means start of the input. !c are literally !c (gmnotes|bio) matches either gmnotes or bio , and is a capturing group. Whatever is in here will be stored at index 1 in the match. (?: ) is a "non-capturing group" which means everything inside it is treated as a single unit for the trailing ? (see below) - in the non-capturing group is just a listeral - (.*) is a capturing group that matches .* , which means any character ( . ) repeated 0 or more times ( * ). Whatever this matches will be stored at index 2. ? means 0 or 1 of the preceding item, the non-capturing group above. (You can play with these regular expressions here: <a href="https://regex101.com/r/X4scRZ/1" rel="nofollow">https://regex101.com/r/X4scRZ/1</a>) The first Regular Expression determines if the command for this script was provided. The second is used to extract 2 bits of information. Which field to extract is stored at index 1 and then set into the variable where , what to match is stored at index 2 and is used to construct a regular expression on line 13 if it's present. Next there is a map and reject on lines 19-20 which convert the collection of tokens into a collection of characters. On line 21, I call the asynchronous .get() to extract whichever field is stored in where and pass it a callback function where the result will be passed in as the argument val . Line 22 checks if val is null . Instead of an empty string, the gmnotes, bio, and handout notes fields will be passed back as null . This is one of several inconsistencies in the API that are a result of backend implementation showing through. Lines 24-27 I just broke that operation onto multiple lines as I made some changes to it. Line 25, I removed the decodeURIComponent() call as the text in these 3 fields is stored differently than on the token GMNotes and that decoding was not needed. Line 26, I added in a replace to strip HTML for the by-line match. On line 30, I striped out another call to decodeURIComponent() . Cheers!