// Utility functions for tag autocomplete // Parse the CSV file into a 2D array. Doesn't use regex, so it is very lightweight. function parseCSV(str) { var arr = []; var quote = false; // 'true' means we're inside a quoted field // Iterate over each character, keep track of current row and column (of the returned array) for (var row = 0, col = 0, c = 0; c < str.length; c++) { var cc = str[c], nc = str[c + 1]; // Current character, next character arr[row] = arr[row] || []; // Create a new row if necessary arr[row][col] = arr[row][col] || ''; // Create a new column (start with empty string) if necessary // If the current character is a quotation mark, and we're inside a // quoted field, and the next character is also a quotation mark, // add a quotation mark to the current column and skip the next character if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; } // If it's just one quotation mark, begin/end quoted field if (cc == '"') { quote = !quote; continue; } // If it's a comma and we're not in a quoted field, move on to the next column if (cc == ',' && !quote) { ++col; continue; } // If it's a newline (CRLF) and we're not in a quoted field, skip the next character // and move on to the next row and move to column 0 of that new row if (cc == '\r' && nc == '\n' && !quote) { ++row; col = 0; ++c; continue; } // If it's a newline (LF or CR) and we're not in a quoted field, // move on to the next row and move to column 0 of that new row if (cc == '\n' && !quote) { ++row; col = 0; continue; } if (cc == '\r' && !quote) { ++row; col = 0; continue; } // Otherwise, append the current character to the current column arr[row][col] += cc; } return arr; } // Load file async function readFile(filePath, json = false, cache = false) { if (!cache) filePath += `?${new Date().getTime()}`; let response = await fetch(`file=${filePath}`); if (response.status != 200) { console.error(`Error loading file "${filePath}": ` + response.status, response.statusText); return null; } if (json) return await response.json(); else return await response.text(); } // Load CSV async function loadCSV(path) { let text = await readFile(path); return parseCSV(text); } // Debounce function to prevent spamming the autocomplete function var dbTimeOut; const debounce = (func, wait = 300) => { return function (...args) { if (dbTimeOut) { clearTimeout(dbTimeOut); } dbTimeOut = setTimeout(() => { func.apply(this, args); }, wait); } } // Difference function to fix duplicates not being seen as changes in normal filter function difference(a, b) { if (a.length == 0) { return b; } if (b.length == 0) { return a; } return [...b.reduce((acc, v) => acc.set(v, (acc.get(v) || 0) - 1), a.reduce((acc, v) => acc.set(v, (acc.get(v) || 0) + 1), new Map()) )].reduce((acc, [v, count]) => acc.concat(Array(Math.abs(count)).fill(v)), []); } function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } function escapeHTML(unsafeText) { let div = document.createElement('div'); div.textContent = unsafeText; return div.innerHTML; } // Queue calling function to process global queues async function processQueue(queue, context, ...args) { for (let i = 0; i < queue.length; i++) { await queue[i].call(context, ...args); } } // The same but with return values async function processQueueReturn(queue, context, ...args) { let qeueueReturns = []; for (let i = 0; i < queue.length; i++) { let returnValue = await queue[i].call(context, ...args); if (returnValue) qeueueReturns.push(returnValue); } return qeueueReturns; } // Specific to tag completion parsers async function processParsers(textArea, prompt) { // Get all parsers that have a successful trigger condition let matchingParsers = PARSERS.filter(parser => parser.triggerCondition()); // Guard condition if (matchingParsers.length === 0) { return null; } let parseFunctions = matchingParsers.map(parser => parser.parse); // Process them and return the results return await processQueueReturn(parseFunctions, null, textArea, prompt); }