Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

[Help] Script that adds table from handout

1603288106

Edited 1603310175
Jordan C.
Pro
API Scripter
Hi all, I recently posted an excel macro that can quickly format tables for use with Table Export and Recursive Tables and after a great suggestion from keithcurtis I am working my way through making it into an API script. What I have so far I literally just started learning JS this week so my progress might be fairly slow and I still have a lot to read through but so far I have a script that can take an input like this:         01-05       1d6+5 Kobolds and 1d4 bandits         06-10 2d4 Bears         11-20 1 Ettin and turn it into this: !import-table-item --TableName --<%%91%%><%%91%%>1d6+5<%%93%%><%%93%%> Kobolds and <%%91%%><%%91%%>1d4<%%93%%><%%93%%> bandits --5 !import-table-item --TableName --<%%91%%><%%91%%>2d4<%%93%%><%%93%%> Bears --5 !import-table-item --TableName --1 Ettin --10 Code:  Below is the code I have so far. I am sure I am making sins related to best practices and such so if anything is offensive to look at please let me know! Disclaimer: The str variable is hardcoded at the moment for my testing purposes but would be intended to receive the chat input. Edit:  Cleaned up the code for manipulating table outputs to make better use of regex     //updated code 10/21/2020 15:55 var diceRoll = /(\d+d\d+(\+\d)?)/g, lCont = "<%%91%%><%%91%%>", rCont = "<%%93%%><%%93%%>"; var str = "01-05\t1d6+5 Kobolds and a bandit\n06-10\t12d4 Bats and 1d4 Bears\n11-20\t1 Ettin"; //replace with str.content.msg var column, weight, tableRow = str.split(/\n/);     //Separate each row by \t to separate column 0 and colummn 1 for (var r = 0; r<tableRow.length; r++) { column = tableRow[r].split(/\t/); (column[1].match(diceRoll) ? column[1].replace(diceRoll, lCont + "$1" + rCont) : ""); //item output     //Find weight of item var rangeDice = column[0].split("-"); (rangeDice[1] !== undefined ? weight = parseInt(rangeDice[1]) - parseInt(rangeDice[0]) + 1 : weight = 1); //weight output } //Original code var diceRoll = /[0-9]d[0-9]/, column, weight, lCont = "<%%91%%><%%91%%>", rCont = "<%%93%%><%%93%%>"; var str = "01-05\t1d6+5 Kobolds and 1d4 bandits\n06-10\t2d4 Bears\n11-20\t1 Ettin"; //replace with str.content.msg var tableRow = str.split(/\n/); //Separate each row by \t to separate column 0 and colummn 1 for (var r = 0; r<tableRow.length; r++) { column = tableRow[r].split(/\t/); //Find weight of item var rangeDice = column[0].split("-"); (rangeDice[1] !== undefined ? weight = parseInt(rangeDice[1]) - parseInt(rangeDice[0]) + 1 : weight = 1); //separte string into substrings and identify if substring is a dice roll var substr = column[1].split(/\s/), i = 0; for (i = 0; i<substr.length; i++) { (substr[i].match(diceRoll) ? substr[i] = lCont + substr[i] + rCont : substr[i] = substr[i]); } //Output as format used for Table Export and Recursive Tables console.log("!import-table-item --TableName --" + substr.join(" ") + " --" + weight) } The next step I have a basic understanding of the chat message event, but to my knowledge each new line in the chat provokes a new chat event which would make pasting the information into chat invalid as it would parse the first line after a command such as "!convert" and then treat the following lines as regular chat sends. I am looking for a way to take data that pastes as multiple lines into the chat box and have them all be considered a single string separated by tabs and new lines. The end goal Now, in the grand scheme of things I would like to take keithcurtis 's suggestion and make it so that the script can see an event of a handout being opened, then read the handout to check for a table. If a table exists: Check that the table meets the criteria of a rollable table Include an "ADD" button to optionally add the table to the game's tables Include a "ROLL" button in the handout to roll from said table. (Either via the table in-game or directly from the data that was collected) As I mentioned, I still have some work to do on my own to handle the data from the events and such but I certainly welcome any advice along the way. If anyone is willing to provide some insight for this endeavor or just provide general advice for going about a project like this I welcome the input. I aim to learn as much of the process as possible, but I am also willing to admit when something is just too far over my head. Thanks!
1603294459

Edited 1603294587
The Aaron
Roll20 Production Team
API Scripter
I'll give you a few thoughts to start you off, and try to post more later.  First, I would suggest bypassing TableExport entirely. If you're already creating an API script, it's much easier to just create Tables and TableRows directly than to create the commands to have TableExport do it. TableExport is solving a different problem, the easy human migration of tables between games. Its use to import custom created tables is actually an emergent functionality. There's no need for your script to be dependent on TableExport, and it will be simpler code on your end.  Second, the text parsing of a Handout is likely going to be the hardest part. The text editor injects a bunch of html which you'll likely need to strip out, and people typing in the tables will have different text than people pasting them in. You'll want to invest heavily in regular expressions to handle the rows. I suggest replacing /<br[/]?>/ with carriage returns, then stripping all html from that text, then split to lines and handle each line individually. Be very clear about the format of text you want to accept and normalize white space (could be spaces, tabs, combinations, etc).  Finally, to pass multi-line text and receive it as a single command, encapsulate the multi-line text in {{ }}. Look at TokenMod's handleInput() function for an example of that. 
1603299227

Edited 1603304767
Jordan C.
Pro
API Scripter
Thanks for the pointers! I think in my head I was outlining the process to make a bunch of individual parts that work on their own and can be combined to work as a whole but to your point it may be unnecessarily complicated to do that.  As for the handout parsing - I definitely can see why it would be the biggest obstacle and I am in the process of getting a more thorough understanding of regex to work through it.  It looks like I am also having trouble correctly identify events (such as when a compendium item is dropped, which is ironic since I can even see that there's a console.log for exactly that event.) which is something I need to read more about before diving in completely but my plan once I have that sorted is to identify the handout with query selectors and determining if it contains an <table> tag and then parse the contents of each <tr> and <td>. I am not too certain how much that conflicts with what you mentioned about the stripping html between the breaks . Edit:  I think I understand what you meant about stripping between the breaks but am curious if it would be feasible/easier to split base on the table tags. I hope that's at least somewhat on the right track? And the multi-line text helps a lot, thank you! 
1603329993

Edited 1603396655
Jordan C.
Pro
API Scripter
Edit: 10/22/2020 3:48 EST Basically realized how I originally intended to extract the html was not compatible with the scripts here so I restarted after learning about handling objects within the Roll20 API and made some more progress.  Right now I have an API script that can scan all handouts and check if they have the table tag and extract the notes from them. I have only tested it with handouts that exclusively have rollable tables so it surely needs more handling for ones that don't. Right now it functions similarly to how my previous code handled the innerHTML so that still needs adjusting for 4 column tables. I haven't yet figured out how to grab the table id, check if an identical table exists, and pass it to the new item object.  Backtracked a little since I didn't really understand the object functions within the API but making some progress. var Handout = findObjs({ type: "handout" }); _.each(Handout, function(obj) { var contents = obj.get("notes", function(notes){ if (notes.includes("<table>")) { var colParse = /<td>/g; var strip = /<[^>]*>/g; var diceRoll = /(\d+d\d+(\+\d)?)/g, leftBracket = "[[", rightBracket = "]]"; var col, weight, range, input, table; //clear arrays?? let Handout = notes; table = Handout.split("<table>"); let countTables = table.length; for (let t = 1; t<countTables; t++) { let row = table[t].split("<tr>"); row.shift(); let countRows = row.length; for (var r = 1; r<countRows; r++) { col = row[r].split(colParse); col[2] = col[2].replace(strip,""), col[1] = col[1].replace(strip, ""); range = col[1].split("-"); (range[1] !== undefined ? weight = parseInt(range[1]) - parseInt(range[0]) + 1 : weight = 1); (col[2].match(diceRoll) ? input = col[2].replace(diceRoll, leftBracket + "$1" + rightBracket) : input = col[2] ); log(input + ", " + weight); createObj("tableitem", { name: input, rollabletableid: "-MJeEFknMh1cm1Ia9oxf", weight: weight }); } } } }); }); Outdated: I believe I have a way of parsing the data for standard rollable tables so far; definitely not the most elegant thing ever but it worked for the tests I ran. I know I need a real way of getting the innerHTML from the handouts which is my next step. I believe using the 'data-handoutid' query selector should work, then searching for a "table" tag. Code: var colParse = /<td>/g; var strip = /<[^>]*>/g; var diceRoll = /(\d+d\d+(\+\d)?)/g, leftBracket = "<%%91%%><%%91%%>", rightBracket = "<%%93%%><%%93%%>"; var col, row, item, countRows, weight, range; countRows = document.body.getElementsByTagName("tr").length; for (var r = 0; r<countRows; r++) { row = document.getElementsByTagName("tr")[r].innerHTML; col = row.split(colParse); col[2] = col[2].replace(strip,""); range = col[1].split("-"); (range[1] !== undefined ? weight = parseInt(range[1]) - parseInt(range[0]) + 1 : weight = 1); (col[2].match(diceRoll) ? input = col[2].replace(diceRoll, leftBracket + "$1" + rightBracket) : input = col[2] ); } This code worked on the following example: <table> <tbody><tr><td><b>d100</b></td><td><b>Encounter</b></td></tr><tr><td>01</td><td>3d8 <strong>scorpions</strong></td></tr><tr><td>02</td><td>2d4 <strong>vultures</strong></td></tr><tr><td>03</td><td>1 abandoned <strong>mule</strong></td></tr><tr><td>04</td><td>2d6 <strong>commoners</strong> with 2d4 <strong>camels</strong> bound for a distant city</td></tr><tr><td>05</td><td>1d6 <strong>flying snakes</strong></td></tr><tr><td>06</td><td>2d6 <strong>hyenas</strong> or 2d6 <strong>jackals</strong></td></tr><tr><td>07</td><td>1d6 <strong>guards</strong> escorting a <strong>noble</strong> to the edge of the desert, all of them astride <strong>camels</strong></td></tr><tr><td>08</td><td>1d6 <strong>cats</strong></td></tr><tr><td>09</td><td>1 <strong>pseudodragon</strong></td></tr><tr><td>10</td><td>1d4 <strong>poisonous snakes</strong></td></tr><tr><td>11-13</td><td>2d4 <strong>stirges</strong></td></tr><tr><td>14-15</td><td>1d6+2 <strong>giant wolf spiders</strong></td></tr><tr><td>16-17</td><td>1 <strong>scout</strong></td></tr><tr><td>18-20</td><td>2d4 <strong>giant poisonous snakes</strong></td></tr><tr><td>21-25</td><td>Single-file tracks marching deeper into the desert</td></tr><tr><td>26-27</td><td>4d4 <strong>kobolds</strong></td></tr><tr><td>28-29</td><td>1 <strong>jackalwere</strong></td></tr><tr><td>30-31</td><td>3d6 <strong>tribal warriors</strong></td></tr><tr><td>32-33</td><td>1d6 <strong>giant lizards</strong></td></tr><tr><td>34-35</td><td>1 <strong>swarm of insects</strong></td></tr><tr><td>36-40</td><td>An oasis surrounded by palm trees and containing the remnants of an old camp</td></tr><tr><td>41-44</td><td>3d6 <strong>bandits</strong></td></tr><tr><td>45-46</td><td>1d4 <strong>constrictor snakes</strong></td></tr><tr><td>47-48</td><td>2d4 <strong>winged kobolds</strong></td></tr><tr><td>49-50</td><td>1 <strong>dust mephit</strong></td></tr><tr><td>51-52</td><td>1d3+1 <strong>giant toads</strong></td></tr><tr><td>53-54</td><td>1d4 <strong>giant spiders</strong></td></tr><tr><td>55</td><td>1 <strong>druid</strong></td></tr><tr><td>56-57</td><td>2d4 <strong>hobgoblins</strong></td></tr><tr><td>58</td><td>1 <strong>wight</strong></td></tr><tr><td>59-60</td><td>1 <strong>ogre</strong></td></tr><tr><td>61-65</td><td>A brass lamp lying on the ground</td></tr><tr><td>66-67</td><td>1d4 <strong>giant vultures</strong></td></tr><tr><td>68</td><td>1 <strong>phase spider</strong></td></tr><tr><td>69</td><td>1 <strong>giant constrictor snake</strong></td></tr><tr><td>70-71</td><td>1 <strong>gnoll pack lord</strong> with 1d3 <strong>giant hyenas</strong></td></tr><tr><td>72</td><td>1d6+2 <strong>gnolls</strong></td></tr><tr><td>73-74</td><td>1 <strong>mummy</strong></td></tr><tr><td>75</td><td>1d3 <strong>half-ogres</strong></td></tr><tr><td>76-80</td><td>A pile of humanoid bones wrapped in rotting cloth</td></tr><tr><td>81-82</td><td>1 <strong>lamia</strong></td></tr><tr><td>83</td><td>1 <strong>hobgoblin captain</strong> with 2d6 <strong>hobgoblins</strong></td></tr><tr><td>84</td><td>2d4 <strong>death dogs</strong></td></tr><tr><td>85-86</td><td>1d4 <strong>giant scorpions</strong></td></tr><tr><td>87</td><td>1 <strong>yuan-ti malison</strong> with 1d4+1 <strong>yuan-ti purebloods</strong></td></tr><tr><td>88-89</td><td>1 <strong>bandit captain</strong> with 1 <strong>druid</strong> and 3d6 <strong>bandits</strong></td></tr><tr><td>90</td><td>2d4 <strong>thri-kreen</strong></td></tr><tr><td>91</td><td>1 <strong>air elemental</strong></td></tr><tr><td>92</td><td>1d3 <strong>couatls</strong></td></tr><tr><td>93</td><td>1 <strong>fire elemental</strong></td></tr><tr><td>94</td><td>1d4 <strong>gnoll fangs of Yeenoghu</strong></td></tr><tr><td>95</td><td>1 <strong>revenant</strong></td></tr><tr><td>96</td><td>1d4 <strong>weretigers</strong></td></tr><tr><td>97</td><td>1 <strong>cyclops</strong></td></tr><tr><td>98</td><td>1 <strong>young brass dragon</strong></td></tr><tr><td>99</td><td>1 <strong>medusa</strong></td></tr><tr><td>00</td><td>1 <strong>yuan-ti abomination</strong></td></tr></tbody> </table> In addition to any comments on functionality I am super open to criticism on cleaning my code up or best practices. Edit (9:56 AM EST, 10/22/2020): I now have a code that can check every open handout for a table, then parse the date returning an input (as item description) and weight (roll weight of item). var colParse = /<td>/g; var strip = /<[^>]*>/g; var diceRoll = /(\d+d\d+(\+\d)?)/g, leftBracket = "<%%91%%><%%91%%>", rightBracket = "<%%93%%><%%93%%>"; var col, weight, range, input, table; var countHandouts = document.body.querySelectorAll("div[data-handoutid]").length; //clear arrays?? for (let h = 0; h<countHandouts; h++) { let Handout = document.body.querySelectorAll("div[data-handoutid]")[h].innerHTML; if (Handout.includes("<table>")) { table = Handout.split("<table>"); } else { break; } let countTables = table.length; for (let t = 1; t<countTables; t++) { let row = table[t].split("<tr>"); row.shift(); let countRows = row.length; // let tableName = for (var r = 1; r<countRows; r++) { col = row[r].split(colParse); col[2] = col[2].replace(strip,""), col[1] = col[1].replace(strip, ""); range = col[1].split("-"); (range[1] !== undefined ? weight = parseInt(range[1]) - parseInt(range[0]) + 1 : weight = 1); (col[2].match(diceRoll) ? input = col[2].replace(diceRoll, leftBracket + "$1" + rightBracket) : input = col[2] ); console.log(input + ", " + weight); } } } Right now this only works on tables with two columns (as opposed to the 4 column tables that use 1 row to define 2 separate rolls), but I will be working on handling those in the future. Likely I will try to have it check the existence of columns 3/4 and match their values against the relevant criteria. Next step is to assign the table name which is stumping me a bit. I want to grab the "<h4>" item that most recently precedes the table being processed but I am not quite sure how to do that yet or if it will stay consistent across other handouts.  Thanks to anyone who provides input in advance!
1603484532
Jordan C.
Pro
API Scripter
Well,  After some learning and swearing I have a bare bones script that works extremely well on Encounter handouts and Background handouts generated from the 5e compendium. I have to add a  /d\d+/  RegEx for checking that the first row (or header depending on format) for a value of (d4,d10,etc.) so it doesn't generate tables from things like class level descriptions.  It also still needs handling for multicolumn rollable tables but I imagine that should fairly simple if I check col[2]+ for not null and go from there. I am almost positive that the code below is horrendous to look at and terribly inefficient but I plan to improve it long-term with things like switch / case functions and better variable handling / readability. Apologies in advance for anyone offended by it. If you intend to test this at all in a game please  make sure it's a testing game because it will analyze every single handout in the game and that can end unfavorably. Notes: Handouts made from the compendium are severely inconsistent. Some have <thead> tags, some use <td> to separate those headers, some use <th>, some use <td> to separate rows, and some literally have blank headers. SOMETIMES, those differences happen in the same handout. Specifically, the sorcerer (rules) handout can bite me.  That said, it was a good learning experience for trying to handle multiple scenarios but I still have work to do in that department. Blank <thead> tags still screw this version up. Long-term, I want to find a way to have an index/cache of handouts that have already been processed so it doesn't spend time it doesn't need to so if anyone has advice on that I'm all ears. I also intend to have an option to active based on the creation of a handout which I believe is possible with the create:handout  event? Anyway, here's the code I have as of now: var Handout = findObjs({ type: "handout" }); _.each(Handout, function(obj) { var nameHandout = obj.get("name"); var contents = obj.get("notes", function(notes){ if (notes.includes("<table>")) { var pullTable = /<h4>(?:(?!<h4>).)*?<\/table>/g; var colParse = /<td>.+?<\/td>/g, tableParse = /<table>.+?<\/table>/g, theaderParse = /<thead>.+?<\/thead>/g, headerParse = /<h4>.+?<\/h4>/g; var strip = /<[^>]*>/g, headerSplit = /<t[hd]>.+?<\/t[hd]>/g, splitByTable = /.+?<\/table>/g, splitByH = /.+?(?:(?!<h4>).)*/g var diceRoll = /(\d+d\d+(\+\d+)?)/g, leftBracket = "[[", rightBracket = "]]", rowParse = /<tr>.+?<\/tr>/g; var col, weight, range, input, table, section, ID=[], tableHeader; let Handout = notes; section = Handout.match(splitByH); var countSections = section.length; for (let c = 0; c<countSections; c++) { let tableExists = section[c].includes("<table>"); if (tableExists) { } else { continue; } table = section[c].match(tableParse); let countTables = table.length; for (let t = 0; t<countTables; t++) { if (table[t].includes("<thead>")) { let headers = table[t].match(theaderParse); let h = headers[0].match(headerSplit); let p = section[c].match(headerParse); let headerCount = p.length let sectionHeader = p[headerCount-1].replace(strip, ""); var tableHeader = nameHandout + " " + h[1].replace(strip, ""); } else { let headers = section[c].match(headerParse); var tableHeader = headers[0].replace(strip, ""); } var findTable = findObjs({type: 'rollabletable', name: tableHeader}); if (findTable.length) { ID = findTable[0].id; continue; } else { newTable = createObj('rollabletable', { name: tableHeader }) ID = newTable.id; } let row = table[t].match(rowParse); row.shift(); let countRows = row.length; for (var r = 1; r<countRows; r++) { col = row[r].match(colParse); col[1] = col[1].replace(strip,""), col[0] = col[0].replace(strip, ""); range = col[0].split("-"); (range[1] !== undefined ? weight = parseInt(range[1]) - parseInt(range[0]) + 1 : weight = 1); (col[1].match(diceRoll) ? input = col[1].replace(diceRoll, leftBracket + "$1" + rightBracket) : input = col[1] ); createObj("tableitem", { name: input, rollabletableid: ID, weight: weight }); } } } } }); });
1603739465

Edited 1603741405
Jordan C.
Pro
API Scripter
Finally worked out handling the mess of table formatting that the 5e compendiums have, which should accommodate the large majority of html tables. I tested this version on many different handouts and none of them broke it; would be very interested in finding a handout that does since I'm sure I missed something. Now that I feel confident about the table parsing I am focusing on making a cache of already scanned handouts by ID and skipping them at the beginning so it doesn't run 50 times to scan one new handout. I would appreciate any help in that department, it's a little over my head at the moment. As is my next step which is going to be adding a "ROLL" button inside the handout that references the rollable table below it and runs a macro that works with recursive tables and wrapping the rolled items with [[ ]] to roll any die rolls inside the returned result.  If anyone would be able to point me in the right direction I would appreciate it. Here's the latest version of code that handles every table in a handout that I've thrown at it. var strip = /<[^>]*>/g, diceRoll = /(\d+d\d+(\+\d+)?)/g, leftBracket = "[[", rightBracket = "]]"; //get Handouts var Handout = findObjs({ type: "handout" }); _.each(Handout, function(obj) { var handoutName = obj.get("name"); var contents = obj.get("notes", function(notes){ if (notes.includes("<table>")) { log("scanning handout: " + handoutName); //Separate handout as sections by <h\d> ?? something that still captures header tags let section = notes.match(/.+?(?:(?!<h\d>).)*/gs); //For each section, for (s = 0; s<section.length; s++) { //If section.includes("<table>") if (section[s].includes("<table>")) { //separate section[] into table[] by /<table>...<\/table>/; pass contents of /<h\d>...</\h\d>/ as sectionHeader[] let table = section[s].match(/<table>.+?<\/table>/gs); let e = section[s].match(/<h\d>.+?<\/h\d>/);           let sectionHeader = e[0].replace(strip, ""); var tableName = "error", tableID; //For each table, for (let t = 0; t < table.length; t++) { let tBody = table[t].match(/<tbody>.+?<\/tbody>/gs); let testHeader = table[t].match(/<t[dh]>.+?<\/t[dh]>/gs), testBody = tBody[0].match(/<tr>.+?<\/tr>/gs); let diceCheck = /d\d+/g; if (!(testHeader[0].match(diceCheck)) && !(testBody[0].match(diceCheck))) { log("no rollable table found"); continue; } //reset r let r = 0 //Table[] contains <thead> AND !(Table[].includes("<thead></thead>")) if (table[t].includes("<thead>")) { if (!(table[t].includes("<thead></thead>"))) { let tableHeader = table[t].match(/<thead>.+?<\/thead>/); let h = tableHeader[0].match(/<t[hd]>.+?<\/t[hd]>/gs); //Strip h[0], Strip h[1] if (h[0] && h[1]){ h[0] = h[0].replace(strip, ""), h[1] = h[1].replace(strip, ""); } else { h[0] = ""; } if (h[0].length == 0) { log("blank header"); } else { log("table name is handoutName and h[1]: " + handoutName + ": " + h[1]); tableName = handoutName + ": " + h[1]; } } //end if includes <thead></thead> let d = table[t].match(/<tr>.+?<\/tr>/gs); let g = d[0].match(/<t[dh]>.+?<\/t[dh]>/gs); (g[1] ? g[1] = g[1].replace(strip, "") : g[1] = g[0].replace(strip, "")); if ((d[0].match(/d\d+/))) { log("starting body at row 0, tableName is: " + handoutName + ": " + g[1]); tableName = handoutName + ": " + g[1]; } else { log("starting body row at 1"); r = 1; tableName = handoutName + ": " + sectionHeader; } } else {//if table doesn't include <thead> tableName = sectionHeader if (!(tableName.match(/\s/))) { log("tableName is handoutName and sectionHeader: " + handoutName + ": " + sectionHeader); tableName = handoutName + ": " + tableName; } else { log("tableName is section header: " + sectionHeader); tableName = sectionHeader; } r = 1 } //check if tableName exists, if so log "table already created with this name" and continue; var findTable = findObjs({type: 'rollabletable', name: tableName}); if (findTable.length) { log("table already created with this name"); continue;             //if tableName doesn't exist, create one with tableName and return 'rollabletableid' as tableID } else { newTable = createObj('rollabletable', { name: tableName }); tableID = newTable.id; // log(ID); log("New table created"); } //extract <tbody>.+?<\/tbody> as tableBody, then separate as row[] by /<tr>.+?<\/tr>/g let tableBody = table[t].match(/<tbody>.+?<\/tbody>/gs); let row = tableBody[0].match(/tr>.+?<\/tr>/gs); let range, rangeB, input, weight, inputB, weightB; //for each row for (r; r < row.length; r++) { let col = row[r].match(/<td>.+?<\/td>/gs); col[0] = col[0].replace(strip, ""), col[1] = col[1].replace(strip, ""); range = col[0].split("-"), (col[2] ? rangeB = col[2].split("-") : null); log("creating new item"); (range[1] !== undefined ? weight = parseInt(range[1]) - parseInt(range[0]) + 1 : weight = 1); (col[1].match(diceRoll) ? input = col[1].replace(diceRoll, leftBracket + "$1" + rightBracket) : input = col[1] );               if (col[2]) { col[2] = col[2].replace(strip, ""), col[3] = col[3].replace(strip, ""); (rangeB[1] !== undefined ? weightB = parseInt(rangeB[1]) - parseInt(rangeB[0]) + 1 : weightB = 1); (col[3].match(diceRoll) ? inputB = col[3].replace(diceRoll, leftBracket + "$1" + rightBracket) : inputB = col[3] ); //add rows second item createObj("tableitem", { name: inputB, rollabletableid: tableID, weight: weightB }); // end create obj B } // end if 3rd column exists //add item to table createObj("tableitem", { name: input, rollabletableid: tableID, weight: weight }); // end create obj A }//end for each row loop }//end table loop? } else { log("section contains no tables, moving to next section"); continue; }// end if section includes table } //end for each section }//end if Handout has <table> }//end obj function );//end obj get });//end for each handout
1603756967
The Aaron
Roll20 Production Team
API Scripter
Ah, nice progress you've been making! I'll try and take a detailed look tomorrow, sorry for the delay. 
1603798695
Jordan C.
Pro
API Scripter
Thanks! No worries at all and certainly no rush!
1603825610

Edited 1603843625
Jordan C.
Pro
API Scripter
I think I've done it! A roll link has now been added to each handout rollable table. It really  needs to be cleaned up, BUT it works. There are also some weird nuances that I'm not sure how to address. For instance, it seemed to restart the handout scan at odd moments when it was meant to progress to the next table or next section; it continues correctly but it is terribly inefficient when that happens. Edit: I think this happens anytime the handout is updated. The output could use some flair and formatting and include recursive table functionality which should be rather simple.   Edit: added recursive table functionality and included lines that can be uncommented to be used for instances without that script. I plan to add separate commands for each but for now that works well. I imagine adding functionality for handout creation event will be simple and I plan to tackle that next. PLEASE TEST WITH CAUTION:  This script will create a table for every single rollable table in every handout of your game (since I can't figure out how to have it only analyze a journal folder; if anyone knows how I'm all ears!).  on('chat:message', function(msg) { if (msg.content !== "!testHandouts") { return; } else { log("begin process"); } var strip = /<[^>]*>/g, diceRoll = /(\d+d\d+(\+\d+)?)/g, leftBracket = "[[", rightBracket = "]]", startPos; var linkStr; //get Handouts var Handout = findObjs({ type: "handout" }); _.each(Handout, function(obj) { var handoutName = obj.get("name"); var contents = obj.get("notes", function(notes){ if (notes.includes("<table>")) { log("scanning handout: " + handoutName); //Separate handout as sections by <h\d> ?? something that still captures header tags let section = notes.match(/.+?(?:(?!<h\d>).)*/gs); //For each section, for (s = 0; s<section.length; s++) { //If section.includes("<table>") if (section[s].includes("<table>")) { //separate section[] into table[] by /<table>...<\/table>/; pass contents of /<h\d>...</\h\d>/ as sectionHeader[] let table = section[s].match(/<table>.+?<\/table>/gs); let e = section[s].match(/<h\d>.+?<\/h\d>/); let sectionHeader = e[0].replace(strip, ""); var tableName = "error", tableID; //For each table, for (let t = 0; t < table.length; t++) { let tBody = table[t].match(/<tbody>.+?<\/tbody>/gs); let testHeader = table[t].match(/<t[dh]>.+?<\/t[dh]>/gs), testBody = tBody[0].match(/<tr>.+?<\/tr>/gs); let diceCheck = /d\d+/g; if (!(testHeader[0].match(diceCheck)) && !(testBody[0].match(diceCheck))) { log("no rollable table found"); continue; } //reset r let r = 0 if (table[t].includes("<thead>")) { if (!(table[t].includes("<thead></thead>"))) { let tableHeader = table[t].match(/<thead>.+?<\/thead>/); let h = tableHeader[0].match(/<t[hd]>.+?<\/t[hd]>/gs); if (h[0] && h[1]){ h[0] = h[0].replace(strip, ""), h[1] = h[1].replace(strip, ""); } else { h[0] = ""; } if (h[0].length == 0) { } else { //tableName = h[1] // log("table name is handoutName and h[1]: " + handoutName + ": " + h[1]); tableName = handoutName + " - " + h[1]; } } //end if includes <thead></thead> let d = table[t].match(/<tr>.+?<\/tr>/gs); let g = d[0].match(/<t[dh]>.+?<\/t[dh]>/gs); (g[1] ? g[1] = g[1].replace(strip, "") : g[1] = g[0].replace(strip, "")); if ((d[0].match(/d\d+/))) { tableName = handoutName + " - " + g[1]; } else { r = 1; tableName = handoutName + " - " + sectionHeader; } } else {//if table doesn't include <thead> tableName = sectionHeader if (!(tableName.match(/\s/))) { // log("tableName is handoutName and sectionHeader: " + handoutName + ": " + sectionHeader); tableName = handoutName + " - " + tableName; } else { // log("tableName is section header: " + sectionHeader); tableName = sectionHeader; } r = 1 } //check if tableName exists, if so log "table already created with this name" and continue; var findTable = findObjs({type: 'rollabletable', name: tableName}); //var linkTable = "<br><a href=\"`[[ 1t[" + tableName + "] ]]\">Roll</a></br>" //non-recursive table declaration             var linkTable = "<br><a href=\"`!rt [[ 1t[" + tableName + "] ]]\">Roll</a></br>" //optional recursive table declaration if (findTable.length) { log("table already created with this name"); if (!(notes.includes(linkTable))) { notes = notes.replace(table[t], linkTable + table[t]); obj.set('notes', notes); } continue; } else { newTable = createObj('rollabletable', { name: tableName }); tableID = newTable.id; log("New table created: " + tableName); } //extract <tbody>.+?<\/tbody> as tableBody, then separate as row[] by /<tr>.+?<\/tr>/g let tableBody = table[t].match(/<tbody>.+?<\/tbody>/gs); let row = tableBody[0].match(/tr>.+?<\/tr>/gs); let range, rangeB, input, weight, inputB, weightB; //for each row for (r; r < row.length; r++) { //separate row[] by /<td>.+?<\/td>/g as col[] let col = row[r].match(/<td>.+?<\/td>/gs); //strip col[0-4] col[0] = col[0].replace(strip, ""), col[1] = col[1].replace(strip, ""); //get weight and input name from columns range = col[0].split("-"), (col[2] ? rangeB = col[2].split("-") : null); log("creating new item"); (range[1] !== undefined ? weight = parseInt(range[1]) - parseInt(range[0]) + 1 : weight = 1); (col[1].match(diceRoll) ? input = col[1].replace(diceRoll, leftBracket + "$1" + rightBracket) : input = col[1] ); //recursive items               //input = col[1] //non-recursive items if (col[2]) { col[2] = col[2].replace(strip, ""), col[3] = col[3].replace(strip, ""); (rangeB[1] !== undefined ? weightB = parseInt(rangeB[1]) - parseInt(rangeB[0]) + 1 : weightB = 1); (col[3].match(diceRoll) ? inputB = col[3].replace(diceRoll, leftBracket + "$1" + rightBracket) : inputB = col[3] ); //recursive items                 //inputB = col[3] //non-recursive items //add rows second item createObj("tableitem", { name: inputB, rollabletableid: tableID, weight: weightB }); // end create obj B } // end if 3rd column exists //add item to table createObj("tableitem", { name: input, rollabletableid: tableID, weight: weight }); // end create obj A }//end for each row loop notes = notes.replace(table[t], linkTable + table[t]); obj.set('notes', notes); continue; }//end table loop? } else { log("section contains no tables, moving to next section"); continue; }// end if section includes table } //end for each section }//end if Handout has <table> }//end obj function );//end obj get });//end for each handout });//end chat I unfortunately don't have the DMG yet so I can't test any tables from there but it has yet to fail on any test runs. If anyone happens to find a fail point I would love to hear where.
1603896610

Edited 1603993849
Jordan C.
Pro
API Scripter
Latest version:  works on handout events such as change:handout  and add:handout  instead of calling it from a chat command. Also  Two links get added:  One for regular rolls, and one for recursive. Both are labelled respectively. The regular roll format is crappy atm, so I'll work on that, but the results function correctly from my testing. I did figure out that changing the handout is what causes it to loop back so I added a single case for setting the new notes section. Still re-loops once but that loop ends nearly immediately. This code is also much safer in terms of mass production of tables; it will only create new tables when a new handout is created that has rollable tables. For instance, dragging "Arctic Encounters (Levels 1-4)" from the 5e Monster Manual compendium into the game will create, then change a handout and that's when the script runs. I believe it will also run after a user created handout is submitted but I have not gotten to that testing part yet. In order to create roll links for each handout in the game that already exists the following lines can be commented out: 1, 9, 14, 254, 258 This bypasses the on('ready') event and the on('change') event so when the game is started it treats every handout as being added again and will rewrite to include the roll links. Simply un-comment the lines to return it to its normal state. Edit:  Removed unnecessary logs/comments and added more descriptive comments where I figured it might help. on('ready', function() { //comment out for first run on('add:handout', function(obj) { var i = 0; log('handout created: ' + obj.get('name')); on('change:handout', function(obj) { //comment out for first run if (i > 0) { return; } i++; //comment out for first run var strip = /<[^>]*>/g, diceRoll = /(\d+d\d+(\+\d+)?)/g, leftBracket = "[[", rightBracket = "]]"; var Handout = getObj('handout', obj.id); var handoutName = obj.get("name"); Handout.get("notes", function(notes){ if (notes.includes("<table>")) { //Separate handout as sections by <h#> tags, keeping tags let section = notes.match(/.+?(?:(?!<h\d>).)*/gs); //For each section, for (s = 0; s<section.length; s++) { //If section has a table if (section[s].includes("<table>")) { //separate section into table[] by <table> tags, pass contents of <h#> tags as sectionHeader let table = section[s].match(/<table>.+?<\/table>/gs); //grabs inner text of table tags let e = section[s].match(/<h\d>.+?<\/h\d>/); //take text such as "<h4>Header</h4>" let sectionHeader = e[0].replace(strip, ""); //strip text of html var tableName = "error", tableID; //keeps tableName defined and labels the table that failed condition tests //For each table, for (let t = 0; t < table.length; t++) { let tBody = table[t].match(/<tbody>.+?<\/tbody>/gs);//grab inner text of tbody tags let testHeader = table[t].match(/<t[dh]>.+?<\/t[dh]>/gs), testBody = tBody[0].match(/<tr>.+?<\/tr>/gs);//header matches on <td> or <th> tags let diceCheck = /d\d+/g;//matches on text such as "d8" or "d20" //check whether table is rollable if (!(testHeader[0].match(diceCheck)) && !(testBody[0].match(diceCheck))) { //check if table is rollable continue; } //reset r let r = 0 //Table[] contains <thead> if (table[t].includes("<thead>")) { if (!(table[t].includes("<thead></thead>"))) { //stable doesn't include thead tags with null text //Separate tableHeader by <thead> tags let tableHeader = table[t].match(/<thead>.+?<\/thead>/); //Separate tableHeader into h by <th> tags let h = tableHeader[0].match(/<t[hd]>.+?<\/t[hd]>/gs); //Strip h[0], Strip h[1] if (h[0] && h[1]){ h[0] = h[0].replace(strip, ""), h[1] = h[1].replace(strip, ""); } else { h[0] = "";//make h[0] not null/undefined but keep length of 0 } if (h[0].length == 0) { //can be changed to if not statement } else { tableName = handoutName + " - " + h[1]; } } //end if includes <thead></thead> //check first row of table for dice, catches cases with blank headers let d = table[t].match(/<tr>.+?<\/tr>/gs); let g = d[0].match(/<t[dh]>.+?<\/t[dh]>/gs); (g[1] ? g[1] = g[1].replace(strip, "") : g[1] = g[0].replace(strip, "")); if ((d[0].match(/d\d+/))) { //matches on text such as "3d4" or "1d6+2" tableName = handoutName + " - " + g[1]; } else { r = 1; tableName = handoutName + " - " + sectionHeader; } } else {//if table doesn't include <thead> tableName = sectionHeader; if (!(tableName.match(/\s/))) { tableName = handoutName + " - " + tableName; } else { tableName = sectionHeader; } r = 1; } //check if tableName exists, if so log "table already created with this name" and continue; var findTable = findObjs({type: 'rollabletable', name: tableName}); var linkTable = "<br><a href=\"`!rt [[ 1t[" + tableName + "] ]]\">Roll Recursive</a></br><br><a href=\"`/r 1t[" + tableName + "]\">Roll</a></br>"; if (findTable.length) { if (!(notes.includes(linkTable))) { notes = notes.replace(table[t], linkTable + table[t]); } continue; } else { //create new table var newTable = createObj('rollabletable', { name: tableName }); tableID = newTable.id; } //extract tableBody by <table> tags, then separate as row[] by <tr> tags let tableBody = table[t].match(/<tbody>.+?<\/tbody>/gs); let row = tableBody[0].match(/tr>.+?<\/tr>/gs); let range, rangeB, input, weight, inputB, weightB; //for each row for (r; r < row.length; r++) { //separate row[] by <td> tags as col[] let col = row[r].match(/<td>.+?<\/td>/gs); //strip col[0-1] of html syntax col[0] = col[0].replace(strip, ""), col[1] = col[1].replace(strip, ""); //get range as weight and item name as input from columns range = col[0].split("-"); (range[1] !== undefined ? weight = parseInt(range[1]) - parseInt(range[0]) + 1 : weight = 1); //finds range of first column (col[1].match(diceRoll) ? input = col[1].replace(diceRoll, leftBracket + "$1" + rightBracket) : input = col[1] ); //surrounds any dice rolls with brackets if (col[2]) { //repeat process if table has four columns rangeB = col[2].split("-"); col[2] = col[2].replace(strip, ""), col[3] = col[3].replace(strip, ""); (rangeB[1] !== undefined ? weightB = parseInt(rangeB[1]) - parseInt(rangeB[0]) + 1 : weightB = 1); (col[3].match(diceRoll) ? inputB = col[3].replace(diceRoll, leftBracket + "$1" + rightBracket) : inputB = col[3] ); //add weight/items from third/fourth column createObj("tableitem", { name: inputB, rollabletableid: tableID, weight: weightB }); // end create obj B } // end if 3rd column exists //add item to table createObj("tableitem", { name: input, rollabletableid: tableID, weight: weight }); // end create obj A }//end for each row loop //replace old notes with new notes that include roll link notes = notes.replace(table[t], linkTable + table[t]); continue; }//end table loop? } else { continue; }// end if section includes table } //end for each section obj.set('notes', notes); }//end if Handout has <table> }//end obj function );//end obj get });//end change:handout - comment out for first run });//end add:handout });//end on ready - comment out for first run