diff --git a/PinTool/1.0.1/PinTool.js b/PinTool/1.0.1/PinTool.js new file mode 100644 index 000000000..1966ac318 --- /dev/null +++ b/PinTool/1.0.1/PinTool.js @@ -0,0 +1,1381 @@ +// Script: PinTool +// By: Keith Curtis +// Contact: https://app.roll20.net/users/162065/keithcurtis +var API_Meta = API_Meta||{}; //eslint-disable-line no-var +API_Meta.PinTool={offset:Number.MAX_SAFE_INTEGER,lineCount:-1}; +{try{throw new Error('');}catch(e){API_Meta.PinTool.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-6);}} + +on("ready", () => +{ + + const version = '1.0.1'; //version number set here + log('-=> PinTool v' + version + ' is loaded. Use !pintool --help for documentation.'); + //1.0.1 Added burndown to many parts to account for timeouts - Thanks to the Aaron + //1.0.0 Debut + + + // ============================================================ + // HELPERS + // ============================================================ + + const scriptName = "PinTool"; + const PINTOOL_HELP_NAME = "Help: PinTool"; + const PINTOOL_HELP_AVATAR = "https://files.d20.io/images/470559564/QxDbBYEhr6jLMSpm0x42lg/original.png?1767857147"; + + const PINTOOL_HELP_TEXT = ` +
+PinTool provides bulk creation, inspection, and modification of map pins. +It also provides commands for conversion of old-style note tokens to new +map pins. +
+ +Base Command: !pintool
--set — Modify properties on one or more pins (selected pins, or all pins on a page).--convert — Convert map tokens into a handout. Can optionally replace existing token pins upon creation.--place — Places pins on the map based on a specified handout and header level.--purge — Removes all tokens on the map similar to the selected token, or pins similar to the selected pin.--help — Open this help handout.Format:
++!pintool --set property|value [property|value ...] [filter|target] ++ +
All supplied properties apply to every pin matched by the filter.
+ +filter|selected — (default) Selected pinsfilter|all — All pins on the current pagefilter|ID ID ID — Space-separated list of pin IDs
+Values are case-sensitive unless otherwise noted.
+Values indicated by "" mean no value.
+Do not type quotation marks.
+See examples at the end of this document.
+
x — Horizontal position on page, in pixelsy — Vertical position on page, in pixelstitle — Title text displayed on the pinnotes — Notes content associated with the pintooltipImage — Roll20 image identifier (URL)link — ID of the linked handout or objectlinkType — handout or ""subLink — Header identifier within the handoutsubLinkType — headerPlayer, headerGM, or ""visibleTo — Overall visibility: all or ""tooltipVisibleTo — Tooltip visibilitynameplateVisibleTo — Nameplate visibilityimageVisibleTo — Image visibilitynotesVisibleTo — Notes visibilitygmNotesVisibleTo — GM Notes visibilityautoNotesType — Controls blockquote-based player visibility:
+ blockquote or ""
+ scale — Range: 0.25 – 2.0imageDesynced — true / falsenotesDesynced — true / falsegmNotesDesynced — true / false+The convert command builds or updates a handout by extracting data +from map tokens. +
+ +Format:
++!pintool --convert key|value key|value ... ++ +
+A single token must be selected. +All tokens on the same page that represent the +same character are processed. +All note pins must represent a common character. +
+ +name|h1–h5title|stringgmnotes|formattooltip|formatbar1_value|formatbar1_max|formatbar2_value|formatbar2_max|formatbar3_value|formatbar3_max|formatFormat may be:
+h1–h6blockquotecodenormalsupernotesGMText|true-----) in a blockquote.
+ If no separator exists, the entire section is wrapped.
+ imagelinks|true[Image] links after images that send them to chat.
+ --.title| values may contain spaces.+The place command creates or replaces map pins on the current page +based on headers found in an existing handout. +
+ +Format:
++!pintool --place name|h1–h4 handout|Exact Handout Name ++ +
name|h1–h4handout|stringsubLinkType|headerPlayer.subLinkType|headerGM.--convert replace|true.+The purge command removes all tokens on the map similar to the selected token (i.e. that represent the same character), or pins similar to the selected pin (i.e. that are linked to the same handout). +
+ +Format:
++!pintool --purge tokens ++ +
tokens or pins!pintool --set scale|1!pintool --set scale|1 filter|all!pintool --set scale|1 filter|-123456789abcd -123456789abce -123456789abcf !pintool --set title|Camp notesVisibleTo|all!pintool --set autoNotesType|!pintool --convert name|h2 title|Goblin Notes gmnotes|blockquote_type and _pageid) cannot be modified.This will permanently delete ${targets.length} pin(s)
+ linked to handout ${_.escape(handoutName)}.
This cannot be undone.
+ ` + ); + return; + } + } + + + + function normalizeForChat(html) + { + return String(html).replace(/\r\n|\r|\n/g, "").trim(); + } + + const sendStyledMessage = (titleOrMessage, messageOrUndefined, isPublic = false) => + { + const css = getCSS(); + let title, message; + + if(messageOrUndefined === undefined) + { + title = scriptName; + message = titleOrMessage; + } + else + { + title = titleOrMessage || scriptName; + message = messageOrUndefined; + } + + message = String(message).replace( + /\[([^\]]+)\]\(([^)]+)\)/g, + (_, label, command) => + `${label}` + ); + + const html = + `/gi); + if(!blocks) return `
${html}`; + + const idx = blocks.findIndex( + b => normalizeVisibleText(b) === "-----" + ); + + // NEW: no separator → everything is player-visible + if(idx === -1) + { + return `
${blocks.join("")}`; + } + + // Separator exists → split as before + const player = blocks.slice(0, idx).join(""); + const gm = blocks.slice(idx + 1).join(""); + + return `
${player}\n${gm}`; + } + + + function downgradeHeaders(html) + { + return html + .replace(/<\s*h[1-2]\b[^>]*>/gi, "
${content}`; + if(format === "code") return `
${_.escape(content)}`;
+ return content;
+ }
+
+ // ---------------- Build output ----------------
+ const output = [];
+ const tokenByName = {}; // NEW: exact name → token
+ const pinsToCreateCache = new Set();
+
+ let workTokensOnPage = tokensOnPage
+ .sort((a, b) => (a.get("name") || "").localeCompare(b.get("name") || "", undefined,
+ {
+ sensitivity: "base"
+ }));
+
+
+ const finishUp = () => {
+ // ---------------- Handout creation ----------------
+ let h = findObjs(
+ {
+ _type: "handout",
+ name: flags.title
+ })[0];
+ if(!h) h = createObj("handout",
+ {
+ name: flags.title
+ });
+
+ h.set("notes", output.join("\n"));
+ const handoutId = h.id;
+
+ sendChat("PinTool", `/w gm Handout "${flags.title}" updated.`);
+
+ if(!replace) return;
+
+ const skipped = [];
+// const headerRegex = new RegExp(`Handout: ${_.escape(handoutName)}
+/gi); - if(!blocks) return `
${html}`; - - const idx = blocks.findIndex( - b => normalizeVisibleText(b) === "-----" - ); - - // NEW: no separator → everything is player-visible - if(idx === -1) - { - return `
${blocks.join("")}`; - } - - // Separator exists → split as before - const player = blocks.slice(0, idx).join(""); - const gm = blocks.slice(idx + 1).join(""); + function normalizeVisibleText(html) + { + return html + .replace(/
${player}\n${gm}`; - } + function applyBlockquoteSplit(html) + { + const blocks = html.match(/
/gi); + if(!blocks) return `
${html}`; + const idx = blocks.findIndex( + b => normalizeVisibleText(b) === "-----" + ); - function downgradeHeaders(html) - { - return html - .replace(/<\s*h[1-2]\b[^>]*>/gi, "
${blocks.join("")}`; + } - function encodeProtocol(url) - { - return url.replace(/^(https?):\/\//i, "$1!!!"); - } + // Separator exists → split as before + const player = blocks.slice(0, idx).join(""); + const gm = blocks.slice(idx + 1).join(""); - function convertImages(html) - { - if(!html) return html; + return `
${player}\n${gm}`; + } - html = html.replace( - /\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)/gi, - (m, alt, url) => - { - const enc = encodeProtocol(url); - let out = - `
${content}`; - if(format === "code") return `
${_.escape(content)}`;
- return content;
+ out += `${content}`; + if(format === "code") return `
${_.escape(content)}`;
+ return content;
+ }
- const tokenName = token.get("name") || "";
- tokenByName[tokenName] = token; // exact string match
+ // ---------------- Build output ----------------
+ const output = [];
+ const tokenByName = {}; // NEW: exact name → token
+ const pinsToCreateCache = new Set();
- output.push(`Handout: ${_.escape(handoutName)}
+Handout: ${_.escape(handoutName)}
-