// Github: https://github.com/shdwjk/Roll20API/blob/master/RecursiveTable/RecursiveTable.js
// By: The Aaron, Arcane Scriptomancer
// Contact: https://app.roll20.net/users/104025/the-aaron
var RecursiveTable = RecursiveTable || (function() {
'use strict';
var version = '0.2.1',
lastUpdate = 1500986989,
schemaVersion = 0.1,
defaults = {
maxdepth: 10,
delimiter: ', ',
dropempty: true,
sort: false,
prefaceuniquespace: false,
emptydefault: '',
who: ''
},
regex = {
rtCmd: /^(!rt)(?:\[([^\]]*)\])?\s*/,
inlineRoll: /\[\[.*\]\]/
},
checkInstall = function() {
log('-=> RecursiveTable v'+version+' <=- ['+(new Date(lastUpdate*1000))+']');
if( ! _.has(state,'RecursiveTable') || state.RecursiveTable.version !== schemaVersion) {
log(' > Updating Schema to v'+schemaVersion+' <');
state.RecursiveTable = {
version: schemaVersion
};
}
},
sendChatP = function(msg){
return new Promise((resolve) =>{
sendChat('',msg.replace(/\[\[\s+/,'[[').replace(/\[\[\s+\[\[/,'[[[['),(res)=>{
resolve(res[0]);
});
});
},
ch = function (c) {
var entities = {
'<' : 'lt',
'>' : 'gt',
"'" : '#39',
'@' : '#64',
'{' : '#123',
'|' : '#124',
'}' : '#125',
'[' : '#91',
']' : '#93',
'&' : 'amp',
'"' : 'quot',
'-' : 'mdash',
' ' : 'nbsp'
};
if(_.has(entities,c) ){
return ('&'+entities[c]+';');
}
return '';
},
esRE = function (s) {
var escapeForRegexp = /(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g;
return s.replace(escapeForRegexp,"\\$1");
},
HE = (function(){
var entities={
//' ' : '&'+'nbsp'+';',
'<' : '&'+'lt'+';',
'>' : '&'+'gt'+';',
"'" : '&'+'#39'+';',
'@' : '&'+'#64'+';',
'{' : '&'+'#123'+';',
'|' : '&'+'#124'+';',
'}' : '&'+'#125'+';',
'[' : '&'+'#91'+';',
']' : '&'+'#93'+';',
'"' : '&'+'quot'+';'
},
re=new RegExp('('+_.map(_.keys(entities),esRE).join('|')+')','g');
return function(s){
return s.replace(re, function(c){ return entities[c] || c; });
};
}()),
makeSuffixer = function(suffix){
let n = 0;
return (val)=>val+suffix.repeat(n++);
},
showHelp = function(who) {
sendChat('','/w "'+who+'" '+
'
'+
'
'+
'RecursiveTable v'+version+
'
'+
'
'+
'
RecursiveTable provides a way to expand the results of Rollable Tables which have inline rolls within them. Now with options and support for whispering Roll Templates.
'+
'
When using Rolltemplates, your message must have at least one {{ that in not coming from a Rollable Table. When using the PrefaceUniqueSpace option, be sure your {{name=something}} is first.
'+
'
'+
'
Commands'+
'
'+
'
!rt[options] [--help| ... ]'+
'
'+
'
Performs all inline rolls, then continues to expand inline rolls (to a maximum depth of around 10).
'+
'
'+
'- '+
'--help '+ch('-')+' Shows the Help screen'+
'
'+
'- '+
'... '+ch('-')+' Anything following !rt will be expanded, then sent to to the chat.'+
'
'+
'
'+
'
'+
'
'+
'
Options - These are inline settings to adjust how the rolls are put together. Options are specified in '+ch('[')+' '+ch(']')+' right after the !rt:
'+
'
'+
'!rt'+ch('[')+'delimiter:-|maxdepth:20'+ch(']')+' something'+
''+
'
Options are separated with the verticle pipe symbol ('+ch('|')+') and have an optional argument separated by a :. Omitting the argument causes true to be used for switch options, or the default value. All Options are case insenstive. Options are one of 3 types: Number (any integer), Boolean (true values: on, yes, y, true. false values: off, no, n, false), or text (any value except ], use \\'+ch('|')+' for '+ch('|')+')
'+
'
'+
'- '+
'MaxDepth '+ch('-')+' Specifies the number of recursions to perform. Default: 10 (Number)'+
'
'+
'- '+
'Delimiter '+ch('-')+' A string of text to put between table items. The special value
BR will cause html line breaks to be used. Default: , (String)'+
' '+
'- '+
'DropEmpty '+ch('-')+' Causes empty table items to be dropped before joining with the delimiter. Default: on (Boolean)'+
'
'+
'- '+
'Sort '+ch('-')+' Causes table items to be sorted before being joined by the delimiter. Note that this happens at a single layer of recursion, so if you have table items made of of lists of table items, the sorting will only be at each level. Default: off (Boolean)'+
'
'+
'- '+
'PrefaceUniqueSpaces '+ch('-')+' Causes the final message to have a unique number of spaces inserted after each
'+ch('{')+ch('{')+'. This is useful if you'+ch("'")+'re building Roll Templates and might have multiple lines with the same label. Default: off (Boolean)'+
' '+
'
'+
'
'+
'
Examples'+
'
'+
'
Basic usage, whispering treasure to the gm.
'+
'
'+
'!rt /w gm '+ch('[')+ch('[')+'1t'+ch('[')+'treasure-table'+ch(']')+ch(']')+ch(']')+
''+
'
Whispering a roll template:
'+
'
'+
'!rt /w gm '+ch('&')+ch('{')+'template:default'+ch('}')+ch('{')+ch('{')+'treasure='+ch('[')+ch('[')+'1t'+ch('[')+'treasure-table'+ch(']')+ch(']')+ch(']')+''+ch('}')+ch('}')+''+
''+
'
Whispering a roll template, with each item on a separate line:
'+
'
'+
'!rt'+ch('[')+'Delimiter:BR'+ch(']')+' /w gm '+ch('&')+ch('{')+'template:default'+ch('}')+ch('{')+ch('{')+'treasure='+ch('[')+ch('[')+'1t'+ch('[')+'treasure-table'+ch(']')+ch(']')+ch(']')+''+ch('}')+ch('}')+''+
''+
'
Whispering a roll template, with each item on a separate line, with empty results replaced by Nothing:
'+
'
'+
'!rt'+ch('[')+'Delimiter:BR|EmptyDefault:Nothing'+ch(']')+' /w gm '+ch('&')+ch('{')+'template:default'+ch('}')+ch('{')+ch('{')+'treasure='+ch('[')+ch('[')+'1t'+ch('[')+'treasure-table'+ch(']')+ch(']')+ch(']')+''+ch('}')+ch('}')+''+
''+
'
Whispering a roll template, with each item on a separate line, with a table that is returning {{label=values'+ch('}')+ch('}')+':
'+
'
'+
'!rt'+ch('[')+'Delimiter:BR|PrefaceUniqueSpace'+ch(']')+' '+ch('&')+ch('{')+'template:default'+ch('}')+ch('{')+ch('{')+'name=Treasure Bundles'+ch('}')+ch('}')+''+ch('[')+ch('[')+'5t'+ch('[')+'treasure-bundle'+ch(']')+ch(']')+ch(']')+
''+
'
'+
'
'+
'
'
);
},
getAsBoolean = function(val,defVal){
let isTrue = _.contains(['on','yes','y','true'],val.toLowerCase()),
isFalse = _.contains(['off','no','n','false'],val.toLowerCase());
log({isTrue,isFalse});
if(isTrue || isFalse){
return !isFalse;
}
return !_.isUndefined(defVal) ? defVal : val;
},
parseOptions = function(cmdOpts){
return _.chain((cmdOpts||'').replace(/((?:\\.|[^|])*)\|/g,'$1\n').replace(/\\/,'').split(/\n/))
.filter((a)=>a.length)
.reduce((m,o)=>{
let tok=o.split(/:/),
c=tok.shift().toLowerCase(),
a=tok.join(':')||true;
switch(c){
case 'maxdepth':
a=parseInt(a,10)||defaults[c];
break;
case 'dropempty':
case 'sort':
case 'prefaceuniquespace':
a=getAsBoolean(a,defaults[c]);
break;
case 'emptydefault':
break;
case 'delimiter':
switch(a.toLowerCase()){
case 'br':
a = '
';
break;
default:
}
break;
}
m[c]=a;
return m;
},_.clone(defaults))
.value();
},
rollAndParseInlines = function(roll,opts){
return new Promise((returnText)=>{
sendChatP(roll)
.then((msg)=>{
parseInlines(msg.inlinerolls,opts)
.then((subs)=>{
returnText(_.reduce(subs,(m,v,k)=>m.replace(k,v),msg.content));
})
.catch((e)=>{
let eRoll=HE(roll);
sendChat(`RecursiveTables`,`/w "${opts.who}" An error occured while filling the results of this roll: ${eRoll}
Error: ${e.message}
`);
});
})
.catch((e)=>{
let eRoll=HE(roll);
sendChat(`RecursiveTables`,`/w "${opts.who}" An error occured parsing this roll: ${eRoll}
Error: ${e.message}
`);
});
});
},
parseInlines = function(inlines,opts){
return new Promise((returnSubs)=>{
let subOpts = _.clone(opts),
subs = {},
context = {},
promises=[];
--subOpts.maxdepth;
_.each(inlines,(rollRecord,msgIdx)=>{
const key=`$[[${msgIdx}]]`,
result = rollRecord.results.total;
context[key]={
result: rollRecord.results.total
};
_.each(rollRecord.results.rolls,(roll)=>{
if(_.has(roll,'table')){
context[key].hasText=false;
context[key].parts=[];
context[key].sentinal=0;
_.each(roll.results, (die,dieIdx)=>{
if(_.has(die,'tableItem') && _.isString(die.tableItem.name) && !die.tableItem.name.match(/^\d+$/)){
if( regex.inlineRoll.test(die.tableItem.name)){
if(subOpts.maxdepth){
++context[key].sentinal;
promises.push(new Promise((done)=>{
rollAndParseInlines(die.tableItem.name,subOpts)
.then((text)=>{
context[key].parts[dieIdx]=text;
--context[key].sentinal;
if(!context[key].sentinal){
subs[key]=_.compose(
(opts.sort ? (x)=>x.sort() : _.identity),
(opts.dropempty ? (x)=>_.filter(x,(v)=>`${v}`.trim().length) : _.identity)
)(context[key].parts).join(opts.delimiter);
}
done(true);
})
.catch((e)=>{
let eRoll=HE(die.tableItem.name);
sendChat(`RecursiveTables`,`/w "${opts.who}" An Error occured with this TableItem: ${eRoll}
Error: ${e.message}
`);
});
}));
} else {
context[key].parts[dieIdx]=`${die.tableItem.name}`.trim()||opts.emptydefault;
}
} else {
context[key].parts[dieIdx]=`${die.tableItem.name}`.trim()||opts.emptydefault;
}
context[key].hasText=true;
} else {
context[key].parts[dieIdx]=die.v;
}
});
if(context[key].hasText){
subs[key]=_.compose(
(opts.sort ? (x)=>x.sort() : _.identity),
(opts.dropempty ? (x)=>_.filter(x,(v)=>`${v}`.trim().length) : _.identity)
)(context[key].parts).join(opts.delimiter);
} else {
subs[key]=result;
}
} else {
subs[key]=result;
}
});
});
if(promises.length){
Promise.all(promises)
.then(()=>{
returnSubs(subs);
})
.catch((e)=>{
let eRoll=HE(_.pluck(inlines,'expression').join(', '));
sendChat(`RecursiveTables`,`/w "${opts.who}" An Error occurred: ${eRoll}
Error: ${e.message}
`);
});
} else {
returnSubs(subs);
}
});
},
parseMessage = function(msg, opts){
parseInlines(msg.inlinerolls, opts)
.then((subs)=>{
msg.content= _.reduce(subs,(m,v,k)=>m.replace(k,v),msg.content);
sendChat(msg.who||'[BLANK]',(msg.content||'[EMPTY]').replace(/\{\{/g,(opts.prefixuniquespaces ? makeSuffixer(' ') : _.identity)));
})
.catch((e)=>{
let eRoll=HE(msg.content);
sendChat(`RecursiveTables`,`/w "${opts.who}" An Error occured with this message: ${eRoll}
Error: ${e.message}
`);
});
},
handleInput = function(msg_orig) {
var msg = _.clone(msg_orig),
args, who, cmd, opts;
try {
if (msg.type !== "api") {
return;
}
who=(getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname');
args = msg.content.split(/\s+/);
cmd = (args.shift().match(regex.rtCmd)||[]).splice(1);
switch(cmd[0]) {
case '!rt':
if('--help' === args[0]){
showHelp(who);
} else {
opts = parseOptions(cmd[1]);
opts.who = who;
msg.content = msg.content.replace(regex.rtCmd,'');
if(_.has(msg,'rolltemplate') && _.isString(msg.rolltemplate) && msg.rolltemplate.length){
msg.content = msg.content.replace(/\{\{/,'&{template:'+msg.rolltemplate+'} {{');
}
parseMessage(msg,opts);
}
break;
}
} catch (e) {
let who=(getObj('player',msg_orig.playerid)||{get:()=>'API'}).get('_displayname');
sendChat('RecursiveTables',`/w "${who}" `+
``+
`
There was an error while trying to run your command:
`+
`
${msg_orig.content}
`+
`
Please
send me this information so I can make sure this doesn't happen again (triple click for easy select in most browsers.):
`+
`
`+
JSON.stringify({msg: msg_orig, stack: e.stack})+
`
`+
`
`
);
}
},
registerEventHandlers = function() {
on('chat:message', handleInput);
};
return {
CheckInstall: checkInstall,
RegisterEventHandlers: registerEventHandlers
};
}());
on('ready',function() {
'use strict';
RecursiveTable.CheckInstall();
RecursiveTable.RegisterEventHandlers();
});