// 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.0', lastUpdate = 1492982764, schemaVersion = 0.1, maxParseDepth = 10, 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+/,'[['),(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: a=a; } 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,idx)=>{ 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: ${die.tableItem.name}
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: ${die.tableItem.name}
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(); });