From 53236d90dd2189aea404b5b825e2d55c0a804436 Mon Sep 17 00:00:00 2001
From: keithcurtis1
Date: Fri, 13 Feb 2026 13:35:23 -0800
Subject: [PATCH 01/10] Add initial implementation of PinNote script
---
PInNote/PinNote.js | 345 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 345 insertions(+)
create mode 100644 PInNote/PinNote.js
diff --git a/PInNote/PinNote.js b/PInNote/PinNote.js
new file mode 100644
index 0000000000..ad8213a89c
--- /dev/null
+++ b/PInNote/PinNote.js
@@ -0,0 +1,345 @@
+// Script: PinNote
+// By: Keith Curtis
+// Contact: https://app.roll20.net/users/162065/keithcurtis
+var API_Meta = API_Meta||{}; //eslint-disable-line no-var
+API_Meta.PinNote={offset:Number.MAX_SAFE_INTEGER,lineCount:-1};
+{try{throw new Error('');}catch(e){API_Meta.PinNote.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-6);}}
+
+
+(() => {
+ 'use strict';
+
+ const version = '1.0.0'; //version number set here
+ log('-=> PinNote v' + version + ' is loaded.');
+ //Changelog
+ //1.0.0 Debut
+
+
+ const SCRIPT_NAME = 'PinNote';
+
+ const isGMPlayer = (playerid) => playerIsGM(playerid);
+
+ const getTemplate = (name) => {
+ if (typeof Supernotes_Templates === 'undefined') {
+ return null;
+ }
+ if (!name) return Supernotes_Templates.generic;
+ const key = name.toLowerCase();
+ return Supernotes_Templates[key] || Supernotes_Templates.generic;
+ };
+
+ const sendGenericError = (msg, text) => {
+ if (typeof Supernotes_Templates === 'undefined') return;
+
+ const t = Supernotes_Templates.generic;
+ sendChat(
+ SCRIPT_NAME,
+ t.boxcode +
+ t.titlecode + SCRIPT_NAME +
+ t.textcode + text +
+ '' +
+ t.footer +
+ ''
+ );
+ };
+
+ /* ============================================================
+ * HEADER COLOR ENFORCEMENT
+ * ============================================================ */
+
+ const enforceHeaderColor = (html, template) => {
+ if (!html) return html;
+
+ const colorMatch = template.textcode.match(/color\s*:\s*([^;"]+)/i);
+ if (!colorMatch) return html;
+
+ const colorValue = colorMatch[1].trim();
+
+ return html.replace(
+ /<(h[1-4])\b([^>]*)>/gi,
+ (match, tag, attrs) => {
+
+ if (/style\s*=/i.test(attrs)) {
+ return `<${tag}${attrs.replace(
+ /style\s*=\s*["']([^"']*)["']/i,
+ (m, styleContent) =>
+ `style="${styleContent}; color: ${colorValue};"`
+ )}>`;
+ }
+
+ return `<${tag}${attrs} style="color: ${colorValue};">`;
+ }
+ );
+ };
+
+ /* ============================================================ */
+
+ const parseArgs = (content) => {
+ const args = {};
+ content.replace(/--([^|]+)\|([^\s]+)/gi, (_, k, v) => {
+ args[k.toLowerCase()] = v.toLowerCase();
+ return '';
+ });
+ return args;
+ };
+
+ const extractHandoutSection = ({ handout, subLink, subLinkType }) => {
+ return new Promise((resolve) => {
+
+ if (!handout) return resolve(null);
+
+ if (!subLink) {
+ const field = subLinkType === 'headerGM' ? 'gmnotes' : 'notes';
+ handout.get(field, (content) => resolve(content || null));
+ return;
+ }
+
+ if (!['headerplayer', 'headergm'].includes(subLinkType?.toLowerCase())) {
+ return resolve(null);
+ }
+
+ const field = subLinkType.toLowerCase() === 'headergm'
+ ? 'gmnotes'
+ : 'notes';
+
+ handout.get(field, (content) => {
+ if (!content) return resolve(null);
+
+ const headerRegex = /<(h[1-4])\b[^>]*>([\s\S]*?)<\/\1>/gi;
+ let match;
+
+ while ((match = headerRegex.exec(content)) !== null) {
+ const tagName = match[1];
+ const innerHTML = match[2];
+ const stripped = innerHTML.replace(/<[^>]+>/g, '');
+
+ if (stripped === subLink) {
+ const level = parseInt(tagName[1], 10);
+ const startIndex = match.index;
+
+ const remainder = content.slice(headerRegex.lastIndex);
+
+ const stopRegex = new RegExp(
+ `]*>`,
+ 'i'
+ );
+
+ const stopMatch = stopRegex.exec(remainder);
+
+ const endIndex = stopMatch
+ ? headerRegex.lastIndex + stopMatch.index
+ : content.length;
+
+ return resolve(content.slice(startIndex, endIndex));
+ }
+ }
+
+ resolve(null);
+ });
+ });
+ };
+
+ const transformBlockquoteMode = (html) => {
+
+ const blockRegex = /]*>([\s\S]*?)<\/blockquote>/gi;
+
+ let match;
+ let lastIndex = 0;
+ let playerContent = '';
+ let gmContent = '';
+ let found = false;
+
+ while ((match = blockRegex.exec(html)) !== null) {
+ found = true;
+ gmContent += html.slice(lastIndex, match.index);
+ playerContent += match[1];
+ lastIndex = blockRegex.lastIndex;
+ }
+
+ gmContent += html.slice(lastIndex);
+
+ if (!found) {
+ return { player: '', gm: html };
+ }
+
+ return { player: playerContent, gm: gmContent };
+ };
+
+ on('chat:message', async (msg) => {
+ if (msg.type !== 'api' || !msg.content.startsWith('!pinnote')) return;
+
+ if (typeof Supernotes_Templates === 'undefined') {
+ sendChat(SCRIPT_NAME, `/w gm PinNote requires Supernotes_Templates to be loaded.`);
+ return;
+ }
+
+ const args = parseArgs(msg.content);
+ const isGM = isGMPlayer(msg.playerid);
+
+ if (!msg.selected || msg.selected.length === 0)
+ return sendGenericError(msg, 'No pin selected.');
+
+ const sel = msg.selected.find(s => s._type === 'pin');
+ if (!sel)
+ return sendGenericError(msg, 'Selected object is not a pin.');
+
+ const pin = getObj('pin', sel._id);
+ if (!pin)
+ return sendGenericError(msg, 'Selected pin could not be resolved.');
+
+ const isSynced =
+ !pin.get('notesDesynced') &&
+ !pin.get('gmNotesDesynced') &&
+ !pin.get('imageDesynced');
+
+ const linkType = pin.get('linkType');
+
+ /* ============================================================
+ * LINKED HANDOUT MODE
+ * ============================================================ */
+
+ if (isSynced && linkType === 'handout') {
+
+ const handoutId = pin.get('link');
+ const subLink = pin.get('subLink');
+ const subLinkType = pin.get('subLinkType');
+ const autoNotesType = pin.get('autoNotesType');
+
+ const handout = getObj('handout', handoutId);
+ if (!handout)
+ return sendGenericError(msg, 'Linked handout not found.');
+
+ let extracted = await extractHandoutSection({
+ handout,
+ subLink,
+ subLinkType
+ });
+
+ if (!extracted)
+ return sendGenericError(msg, 'Requested section not found in handout.');
+
+ const template = getTemplate(args.template);
+ if (!template) return;
+
+ const sender = pin.get('title') || SCRIPT_NAME;
+ const titleText = subLink || sender;
+
+ if (subLink) {
+ const headerStripRegex = /^]*>[\s\S]*?<\/h[1-4]>/i;
+ extracted = extracted.replace(headerStripRegex, '');
+ }
+
+ let to = (args.to || 'pc').toLowerCase();
+ if (!isGM) to = 'pc';
+
+ let whisperPrefix = '';
+ const extractingGM = subLinkType?.toLowerCase() === 'headergm';
+
+ let visibleContent = extracted;
+ let gmBlock = '';
+
+ if (autoNotesType === 'blockquote') {
+
+ const transformed = transformBlockquoteMode(extracted);
+
+ visibleContent = enforceHeaderColor(transformed.player, template);
+
+ if (transformed.gm && to !== 'pc') {
+ gmBlock =
+ `` +
+ enforceHeaderColor(transformed.gm, template) +
+ `
`;
+ }
+
+ } else {
+ visibleContent = enforceHeaderColor(visibleContent, template);
+ }
+
+ if (extractingGM) {
+ whisperPrefix = '/w gm ';
+ } else if (to === 'gm') {
+ whisperPrefix = '/w gm ';
+ } else if (to === 'self') {
+ whisperPrefix = `/w "${msg.who}" `;
+ }
+
+ const html =
+ template.boxcode +
+ template.titlecode + titleText +
+ template.textcode +
+ (visibleContent || '') +
+ gmBlock +
+ '' +
+ template.footer +
+ '';
+
+ sendChat(sender, whisperPrefix + html);
+ return;
+ }
+
+ /* ============================================================
+ * CUSTOM PIN MODE
+ * ============================================================ */
+
+ if (
+ !pin.get('notesDesynced') &&
+ !pin.get('gmNotesDesynced') &&
+ !pin.get('imageDesynced')
+ ) {
+ return sendGenericError(
+ msg,
+ 'This pin is not desynced from its linked handout.'
+ );
+ }
+
+ const notes = (pin.get('notes') || '').trim();
+ if (!notes)
+ return sendGenericError(msg, 'This pin has no notes to display.');
+
+ let to = (args.to || 'pc').toLowerCase();
+ if (!isGM) to = 'pc';
+
+ let whisperPrefix = '';
+ if (to === 'gm') whisperPrefix = '/w gm ';
+ else if (to === 'self') whisperPrefix = `/w "${msg.who}" `;
+
+ const template = getTemplate(args.template);
+ if (!template) return;
+
+ const sender = pin.get('title') || SCRIPT_NAME;
+
+ let imageBlock = '';
+ const tooltipImage = pin.get('tooltipImage');
+ if (tooltipImage) {
+ imageBlock =
+ `
`;
+ }
+
+ const coloredNotes = enforceHeaderColor(notes, template);
+
+ let gmBlock = '';
+ if (isGM && to !== 'pc' && pin.get('gmNotes')) {
+ gmBlock =
+ `` +
+ enforceHeaderColor(pin.get('gmNotes'), template) +
+ `
`;
+ }
+
+ const html =
+ template.boxcode +
+ template.titlecode + sender +
+ template.textcode +
+ imageBlock +
+ coloredNotes +
+ gmBlock +
+ '' +
+ template.footer +
+ '';
+
+ sendChat(sender, whisperPrefix + html);
+ });
+
+})();
+
+{try{throw new Error('');}catch(e){API_Meta.PinNote.lineCount=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-API_Meta.PinNote.offset);}}
From bdbfa8d1e42343fb099cfd2297d329d7fdb3b705 Mon Sep 17 00:00:00 2001
From: keithcurtis1
Date: Fri, 13 Feb 2026 13:36:30 -0800
Subject: [PATCH 02/10] Add initial version of PinNote script
---
PInNote/1.0.0/PinNote.js | 345 +++++++++++++++++++++++++++++++++++++++
1 file changed, 345 insertions(+)
create mode 100644 PInNote/1.0.0/PinNote.js
diff --git a/PInNote/1.0.0/PinNote.js b/PInNote/1.0.0/PinNote.js
new file mode 100644
index 0000000000..ad8213a89c
--- /dev/null
+++ b/PInNote/1.0.0/PinNote.js
@@ -0,0 +1,345 @@
+// Script: PinNote
+// By: Keith Curtis
+// Contact: https://app.roll20.net/users/162065/keithcurtis
+var API_Meta = API_Meta||{}; //eslint-disable-line no-var
+API_Meta.PinNote={offset:Number.MAX_SAFE_INTEGER,lineCount:-1};
+{try{throw new Error('');}catch(e){API_Meta.PinNote.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-6);}}
+
+
+(() => {
+ 'use strict';
+
+ const version = '1.0.0'; //version number set here
+ log('-=> PinNote v' + version + ' is loaded.');
+ //Changelog
+ //1.0.0 Debut
+
+
+ const SCRIPT_NAME = 'PinNote';
+
+ const isGMPlayer = (playerid) => playerIsGM(playerid);
+
+ const getTemplate = (name) => {
+ if (typeof Supernotes_Templates === 'undefined') {
+ return null;
+ }
+ if (!name) return Supernotes_Templates.generic;
+ const key = name.toLowerCase();
+ return Supernotes_Templates[key] || Supernotes_Templates.generic;
+ };
+
+ const sendGenericError = (msg, text) => {
+ if (typeof Supernotes_Templates === 'undefined') return;
+
+ const t = Supernotes_Templates.generic;
+ sendChat(
+ SCRIPT_NAME,
+ t.boxcode +
+ t.titlecode + SCRIPT_NAME +
+ t.textcode + text +
+ '' +
+ t.footer +
+ ''
+ );
+ };
+
+ /* ============================================================
+ * HEADER COLOR ENFORCEMENT
+ * ============================================================ */
+
+ const enforceHeaderColor = (html, template) => {
+ if (!html) return html;
+
+ const colorMatch = template.textcode.match(/color\s*:\s*([^;"]+)/i);
+ if (!colorMatch) return html;
+
+ const colorValue = colorMatch[1].trim();
+
+ return html.replace(
+ /<(h[1-4])\b([^>]*)>/gi,
+ (match, tag, attrs) => {
+
+ if (/style\s*=/i.test(attrs)) {
+ return `<${tag}${attrs.replace(
+ /style\s*=\s*["']([^"']*)["']/i,
+ (m, styleContent) =>
+ `style="${styleContent}; color: ${colorValue};"`
+ )}>`;
+ }
+
+ return `<${tag}${attrs} style="color: ${colorValue};">`;
+ }
+ );
+ };
+
+ /* ============================================================ */
+
+ const parseArgs = (content) => {
+ const args = {};
+ content.replace(/--([^|]+)\|([^\s]+)/gi, (_, k, v) => {
+ args[k.toLowerCase()] = v.toLowerCase();
+ return '';
+ });
+ return args;
+ };
+
+ const extractHandoutSection = ({ handout, subLink, subLinkType }) => {
+ return new Promise((resolve) => {
+
+ if (!handout) return resolve(null);
+
+ if (!subLink) {
+ const field = subLinkType === 'headerGM' ? 'gmnotes' : 'notes';
+ handout.get(field, (content) => resolve(content || null));
+ return;
+ }
+
+ if (!['headerplayer', 'headergm'].includes(subLinkType?.toLowerCase())) {
+ return resolve(null);
+ }
+
+ const field = subLinkType.toLowerCase() === 'headergm'
+ ? 'gmnotes'
+ : 'notes';
+
+ handout.get(field, (content) => {
+ if (!content) return resolve(null);
+
+ const headerRegex = /<(h[1-4])\b[^>]*>([\s\S]*?)<\/\1>/gi;
+ let match;
+
+ while ((match = headerRegex.exec(content)) !== null) {
+ const tagName = match[1];
+ const innerHTML = match[2];
+ const stripped = innerHTML.replace(/<[^>]+>/g, '');
+
+ if (stripped === subLink) {
+ const level = parseInt(tagName[1], 10);
+ const startIndex = match.index;
+
+ const remainder = content.slice(headerRegex.lastIndex);
+
+ const stopRegex = new RegExp(
+ `]*>`,
+ 'i'
+ );
+
+ const stopMatch = stopRegex.exec(remainder);
+
+ const endIndex = stopMatch
+ ? headerRegex.lastIndex + stopMatch.index
+ : content.length;
+
+ return resolve(content.slice(startIndex, endIndex));
+ }
+ }
+
+ resolve(null);
+ });
+ });
+ };
+
+ const transformBlockquoteMode = (html) => {
+
+ const blockRegex = /]*>([\s\S]*?)<\/blockquote>/gi;
+
+ let match;
+ let lastIndex = 0;
+ let playerContent = '';
+ let gmContent = '';
+ let found = false;
+
+ while ((match = blockRegex.exec(html)) !== null) {
+ found = true;
+ gmContent += html.slice(lastIndex, match.index);
+ playerContent += match[1];
+ lastIndex = blockRegex.lastIndex;
+ }
+
+ gmContent += html.slice(lastIndex);
+
+ if (!found) {
+ return { player: '', gm: html };
+ }
+
+ return { player: playerContent, gm: gmContent };
+ };
+
+ on('chat:message', async (msg) => {
+ if (msg.type !== 'api' || !msg.content.startsWith('!pinnote')) return;
+
+ if (typeof Supernotes_Templates === 'undefined') {
+ sendChat(SCRIPT_NAME, `/w gm PinNote requires Supernotes_Templates to be loaded.`);
+ return;
+ }
+
+ const args = parseArgs(msg.content);
+ const isGM = isGMPlayer(msg.playerid);
+
+ if (!msg.selected || msg.selected.length === 0)
+ return sendGenericError(msg, 'No pin selected.');
+
+ const sel = msg.selected.find(s => s._type === 'pin');
+ if (!sel)
+ return sendGenericError(msg, 'Selected object is not a pin.');
+
+ const pin = getObj('pin', sel._id);
+ if (!pin)
+ return sendGenericError(msg, 'Selected pin could not be resolved.');
+
+ const isSynced =
+ !pin.get('notesDesynced') &&
+ !pin.get('gmNotesDesynced') &&
+ !pin.get('imageDesynced');
+
+ const linkType = pin.get('linkType');
+
+ /* ============================================================
+ * LINKED HANDOUT MODE
+ * ============================================================ */
+
+ if (isSynced && linkType === 'handout') {
+
+ const handoutId = pin.get('link');
+ const subLink = pin.get('subLink');
+ const subLinkType = pin.get('subLinkType');
+ const autoNotesType = pin.get('autoNotesType');
+
+ const handout = getObj('handout', handoutId);
+ if (!handout)
+ return sendGenericError(msg, 'Linked handout not found.');
+
+ let extracted = await extractHandoutSection({
+ handout,
+ subLink,
+ subLinkType
+ });
+
+ if (!extracted)
+ return sendGenericError(msg, 'Requested section not found in handout.');
+
+ const template = getTemplate(args.template);
+ if (!template) return;
+
+ const sender = pin.get('title') || SCRIPT_NAME;
+ const titleText = subLink || sender;
+
+ if (subLink) {
+ const headerStripRegex = /^]*>[\s\S]*?<\/h[1-4]>/i;
+ extracted = extracted.replace(headerStripRegex, '');
+ }
+
+ let to = (args.to || 'pc').toLowerCase();
+ if (!isGM) to = 'pc';
+
+ let whisperPrefix = '';
+ const extractingGM = subLinkType?.toLowerCase() === 'headergm';
+
+ let visibleContent = extracted;
+ let gmBlock = '';
+
+ if (autoNotesType === 'blockquote') {
+
+ const transformed = transformBlockquoteMode(extracted);
+
+ visibleContent = enforceHeaderColor(transformed.player, template);
+
+ if (transformed.gm && to !== 'pc') {
+ gmBlock =
+ `` +
+ enforceHeaderColor(transformed.gm, template) +
+ `
`;
+ }
+
+ } else {
+ visibleContent = enforceHeaderColor(visibleContent, template);
+ }
+
+ if (extractingGM) {
+ whisperPrefix = '/w gm ';
+ } else if (to === 'gm') {
+ whisperPrefix = '/w gm ';
+ } else if (to === 'self') {
+ whisperPrefix = `/w "${msg.who}" `;
+ }
+
+ const html =
+ template.boxcode +
+ template.titlecode + titleText +
+ template.textcode +
+ (visibleContent || '') +
+ gmBlock +
+ '' +
+ template.footer +
+ '';
+
+ sendChat(sender, whisperPrefix + html);
+ return;
+ }
+
+ /* ============================================================
+ * CUSTOM PIN MODE
+ * ============================================================ */
+
+ if (
+ !pin.get('notesDesynced') &&
+ !pin.get('gmNotesDesynced') &&
+ !pin.get('imageDesynced')
+ ) {
+ return sendGenericError(
+ msg,
+ 'This pin is not desynced from its linked handout.'
+ );
+ }
+
+ const notes = (pin.get('notes') || '').trim();
+ if (!notes)
+ return sendGenericError(msg, 'This pin has no notes to display.');
+
+ let to = (args.to || 'pc').toLowerCase();
+ if (!isGM) to = 'pc';
+
+ let whisperPrefix = '';
+ if (to === 'gm') whisperPrefix = '/w gm ';
+ else if (to === 'self') whisperPrefix = `/w "${msg.who}" `;
+
+ const template = getTemplate(args.template);
+ if (!template) return;
+
+ const sender = pin.get('title') || SCRIPT_NAME;
+
+ let imageBlock = '';
+ const tooltipImage = pin.get('tooltipImage');
+ if (tooltipImage) {
+ imageBlock =
+ `
`;
+ }
+
+ const coloredNotes = enforceHeaderColor(notes, template);
+
+ let gmBlock = '';
+ if (isGM && to !== 'pc' && pin.get('gmNotes')) {
+ gmBlock =
+ `` +
+ enforceHeaderColor(pin.get('gmNotes'), template) +
+ `
`;
+ }
+
+ const html =
+ template.boxcode +
+ template.titlecode + sender +
+ template.textcode +
+ imageBlock +
+ coloredNotes +
+ gmBlock +
+ '' +
+ template.footer +
+ '';
+
+ sendChat(sender, whisperPrefix + html);
+ });
+
+})();
+
+{try{throw new Error('');}catch(e){API_Meta.PinNote.lineCount=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-API_Meta.PinNote.offset);}}
From 375ae94b39ba63b454573b5e8bb469ad83ca53dd Mon Sep 17 00:00:00 2001
From: keithcurtis1
Date: Fri, 13 Feb 2026 13:37:19 -0800
Subject: [PATCH 03/10] Add readme for PinNote script
Added documentation for the PinNote script, including usage instructions, arguments, and examples.
---
PInNote/readme.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 83 insertions(+)
create mode 100644 PInNote/readme.md
diff --git a/PInNote/readme.md b/PInNote/readme.md
new file mode 100644
index 0000000000..fe52012fa8
--- /dev/null
+++ b/PInNote/readme.md
@@ -0,0 +1,83 @@
+# PinNote Script
+
+The **PinNote** script sends information from linked or custom map pins to chat using any Supernotes template.
+You must have Supernotes installed.
+
+---
+
+## Arguments
+
+Arguments are case-insensitive and use the format:
+
+```
+--key|value
+```
+
+---
+
+### `--to|`
+
+Controls where the message is sent.
+
+#### `--to|pc`
+
+Sends a public message to chat.
+
+- GM notes are **never included**.
+
+---
+
+#### `--to|gm`
+
+Whispers the message to the GM only.
+
+- GM notes **are included**.
+
+---
+
+#### `--to|self`
+
+Whispers the message to the invoking player.
+
+- GM notes are included **only if the invoker is a GM**.
+
+---
+
+If a non-GM runs the command, `--to` is ignored and treated as `pc`.
+
+---
+
+### `--template|` (optional)
+
+Selects a Supernotes display template.
+
+- If omitted or invalid, the **generic** template is used silently.
+
+---
+
+## Examples
+
+```
+!pinnote
+!pinnote --to|gm
+!pinnote --to|self --template|dark
+!pinnote --template|wizard
+```
+
+---
+
+## Requirements
+
+- Exactly **one map pin** must be selected.
+ - If none are selected, the script reports an error.
+ - If multiple are selected, only the first pin is used.
+
+- The pin may be:
+ - A **linked pin** (pulls data from its linked handout), or
+ - A **custom pin** (uses its Notes field).
+
+- A custom pin must contain notes.
+ - If the Notes field is empty, nothing is sent and an error is shown.
+
+- **Supernotes must be installed.**
+ - If missing, the script exits and notifies the GM.
From b4cd684b61fdcb49a16e4b24784caf5d9d753f8b Mon Sep 17 00:00:00 2001
From: keithcurtis1
Date: Fri, 13 Feb 2026 13:38:01 -0800
Subject: [PATCH 04/10] Add script.json for PinNote script configuration
This JSON file defines the PinNote script, including its name, version, description, arguments, examples, requirements, and author details.
---
PInNote/script.json | 15 +++++++++++++++
1 file changed, 15 insertions(+)
create mode 100644 PInNote/script.json
diff --git a/PInNote/script.json b/PInNote/script.json
new file mode 100644
index 0000000000..9c34fa4f39
--- /dev/null
+++ b/PInNote/script.json
@@ -0,0 +1,15 @@
+{
+ "name": "PinNote",
+ "script": "PinNote.js",
+ "version": "1.0.0",
+ "description": "# PinNote\n\nPinNote sends information from linked or custom map pins to chat using any Supernotes template. Supernotes must be installed for this script to function.\n\n---\n\n## Arguments\n\nArguments are case-insensitive and use the format:\n\n```\n--key|value\n```\n\n---\n\n### --to|\n\nControls where the message is sent.\n\n#### --to|pc\n\nSends a public message to chat.\n\n- GM notes are never included.\n\n---\n\n#### --to|gm\n\nWhispers the message to the GM only.\n\n- GM notes are included.\n\n---\n\n#### --to|self\n\nWhispers the message to the invoking player.\n\n- GM notes are included only if the invoker is a GM.\n\n---\n\nIf a non-GM runs the command, --to is ignored and treated as pc.\n\n---\n\n### --template| (optional)\n\nSelects a Supernotes display template.\n\n- If omitted or invalid, the generic template is used silently.\n\n---\n\n## Examples\n\n```\n!pinnote\n!pinnote --to|gm\n!pinnote --to|self --template|dark\n!pinnote --template|wizard\n```\n\n---\n\n## Requirements\n\n- Exactly one map pin must be selected.\n - If none are selected, the script reports an error.\n - If multiple are selected, only the first pin is used.\n\n- The pin may be a linked pin or a custom pin.\n - If linked to a handout, the script pulls the relevant section from the handout.\n - If custom, the script uses the Notes field of the pin.\n\n- A custom pin must contain notes.\n - If the Notes field is empty, nothing is sent and an error is shown.\n\n- Supernotes must be installed.\n - If missing, the script exits and notifies the GM.\n\n---\n\nType **!pinnote** in chat to use the script.",
+ "authors": "Keith Curtis",
+ "roll20userid": "162065",
+ "dependencies": ["Supernotes"],
+ "modifies": {
+ "pin": "read",
+ "handout": "read"
+ },
+ "conflicts": [],
+ "previousversions": ["1.0.0"]
+}
From 3f67063c9ed006a770e483f9d435ed952d50e86d Mon Sep 17 00:00:00 2001
From: keithcurtis1
Date: Mon, 16 Feb 2026 13:45:37 -0800
Subject: [PATCH 05/10] Rework help system and fix logic in card output
Updated the help system to use a handout and fixed a logic issue in card output.
---
Supernotes/Supernotes.js | 447 +++++++++++++++++++++++++++++++++++++--
1 file changed, 427 insertions(+), 20 deletions(-)
diff --git a/Supernotes/Supernotes.js b/Supernotes/Supernotes.js
index 08b4c15a15..a8b8bd6174 100644
--- a/Supernotes/Supernotes.js
+++ b/Supernotes/Supernotes.js
@@ -1,14 +1,7 @@
-var API_Meta = API_Meta || {};
-API_Meta.Supernotes = {
- offset: Number.MAX_SAFE_INTEGER,
- lineCount: -1
-}; {
- try {
- throw new Error('');
- } catch (e) {
- API_Meta.Supernotes.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (7));
- }
-}
+var API_Meta = API_Meta||{};
+API_Meta.Supernotes={offset:Number.MAX_SAFE_INTEGER,lineCount:-1};
+{try{throw new Error('');}catch(e){API_Meta.Supernotes.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-3);}}
+
// Supernotes_Templates can be called by other scripts. At this point ScriptCards is the only One Click script that does this.
let Supernotes_Templates = {
@@ -385,6 +378,337 @@ on('ready', function() {
});
on('ready', () => {
+
+
+ /* =========================================================
+ * Supernotes Help Handout Builder
+ * ========================================================= */
+
+const buildSupernotesHelp = () => {
+
+ const HANDOUT_NAME = "Help: Supernotes";
+ const HANDOUT_AVATAR = "https://files.d20.io/images/470559564/QxDbBYEhr6jLMSpm0x42lg/original.png?1767857147"; // change if desired
+
+const helpHtml = `
+Supernotes
+Documentation for v.${version}
+
+
+
+Overview
+
+
+Supernotes pulls content from a token’s GM Notes field and from other character fields not normally accessible to macros.
+If a token represents a character, you may retrieve:
+
+
+
+- Character GM Notes
+- Character Bio
+- Character Avatar
+- Bio images (single, indexed, or all)
+- Token tooltip
+- Token image
+
+
+
+Notes may be whispered to the GM, sent to all players, whispered to the sender, or written directly to a named handout.
+A footer button may optionally appear on GM whispers, allowing the note to be forwarded to players.
+
+
+
+Images, API command buttons, links, markdown image syntax [x](imageURL), and most special characters pass through correctly in both chat and handouts.
+
+
+
+Special Control Character for Inline GMnotes
+-----
+Five dashes placed in the gmnotes of a token indicate that any following content is trested as gm-only text when sent to chat.
+
+
+
+
+Commands
+
+!gmnote
+Whispers note to GM.
+
+!pcnote
+Sends note to all players.
+
+!selfnote
+Whispers note to the command sender.
+
+
+
+Parameters
+
+Sources
+
+- --token
+Pull from selected token GM Notes (default). Token does not require a character.
+
+- --charnote
+Pull from represented character GM Notes.
+
+- --bio
+Pull from character Bio field.
+
+- --avatar
+Return character Avatar image.
+
+- --image
+Return first Bio image.
+
+- --images
+Return all Bio images.
+
+- --image[number]
+Return indexed Bio image (e.g. --image1, --image2).
+
+- --tooltip
+Return selected token tooltip.
+
+- --tokenimage
+Return selected token image.
+
+- --card
+Return token image and gmnotes in one report.
+
+
+
+Options
+
+
+- --notitle
+Suppress title in chat output. May be added to any command in any order.
+
+- --idTOKENID
+Read notes from specific token ID. No space after --id. Example:
!gmnote --id-1234567890abcdef
+
+- --handout|Handout Name|
+Send output to named handout instead of chat.
+Creates the handout if it does not exist.
+Content above the automatic horizontal rule remains persistent.
+
+
+- --help
+Displays help.
+
+- --config
+Opens configuration dialog.
+
+
+
+
+Examples
+
+!pcnote --bio
+Sends selected character Bio to all players.
+
+!gmnote --charnote
+Whispers character GM Notes to GM.
+
+!pcnote --image --notitle
+Sends first image without revealing title.
+
+
+
+Templates
+
+
+Add a template using:
+
+
+--template|templatename
+
+
+Example:
+
+
+!gmnote --template|crt
+!pcnote --template|notebook --bio
+!pcnote --template|faraway --tokenimage
+
+
+All templates include inline buttons and support Send to Players and Make Handout.
+Handouts use Roll20’s native styling for cross-platform reliability.
+
+
+
+
+Available Templates
+
+
+
+ generic Minimal presentation |
+ dark Inverted generic |
+ crt Retro greenscreen |
+
+
+
+ notebook Modern supernatural |
+ gothic Noir horror |
+ apoc Post-apocalyptic |
+
+
+
+ scroll Fantasy parchment |
+ scroll2 Even scrollier |
+ lcars Starship interface |
+
+
+
+ faraway Space opera |
+ steam Steampunk brass |
+ western Frontier theme |
+
+
+
+ dragon Three-fivey style |
+ wizard Fifth edition flavor |
+ strange Bikes & mystery |
+
+
+
+ gate3 GOTY-inspired |
+ choices Alternate GOTY style |
+ roll20light Authority feel |
+
+
+
+ roll20dark Authority, darker |
+ news Bleeding newsprint |
+ treasure Loot listing |
+
+
+
+ vault Comforting shelter |
+ path PF2 Adventure Path |
+ osrblue Yellowed graph paper |
+
+
+
+ roman Bonus classical style |
+ |
+ |
+
+
+
+
+
+Script Synergy
+
+
+Templates are exposed under the object Supernotes_Templates for use in other scripts such as ScriptCards and PinNote .
+Supernotes also integrates amny of it functions with Reporter .
+
+
+For the convenience of coders who wish to integrate their scripts to use Supernotes, the CSS for the tempaltes is exposed to the global space as Supernotes_Templates.templatename. These can be found at the top of the script. Contact keithcurtis if anything is unclear.
+
+
+
+
+
+Configuration
+
+
+On installation, Supernotes defaults to the Default roll template.
+The configuration dialog allows you to:
+
+
+
+- Select a sheet roll template
+- Toggle the “Send to Players” footer button
+
+
+
+Supported sheet templates include:
+
+
+
+- Default Template
+- D&D 5th Edition by Roll20
+- 5e Shaped
+- Pathfinder by Roll20
+- Pathfinder Community
+- Pathfinder 2e by Roll20
+- Starfinder
+
+
+
+
+Troubleshooting
+
+
+If you experience template issues or configuration problems, you may use the buttons below to restore default behavior or re-open the configuration dialog.
+
+
+
+
+
+Restore Default Template resets Supernotes to the Default roll template.
+Re-Run Configuration opens the configuration dialog to select a sheet template and toggle footer options.
+
+
+`;
+
+
+ // Find existing handout
+ let handout = findObjs({
+ _type: "handout",
+ name: HANDOUT_NAME
+ })[0];
+
+ // Create if missing
+ if (!handout) {
+ handout = createObj("handout", {
+ name: HANDOUT_NAME,
+ archived: false
+ });
+ }
+
+ // Always overwrite content + avatar
+ handout.set({
+ notes: helpHtml,
+ avatar: HANDOUT_AVATAR
+ });
+
+ const link = `http://journal.roll20.net/handout/${handout.get("_id")}`;
+
+ const box =
+ ``;
+
+ sendChat("Supernotes", `/w gm ${box}`, null, { noarchive: true });
+};
+
function parseMarkdown(markdownText) {
const htmlText = markdownText
@@ -405,14 +729,29 @@ function cleanText(text,buttonStyle){
text = ((undefined !== text) ? text.replace(/\[([^\]]*?)\]\(([^\)]*?)\)(?$1").replace(//gm, "").replace(/<\/p>/gm, "
").replace("padding:5px'>
", "padding:5px'>") : "");
text = text.replace('
+ .replace(/\r?\n+/g, "
")
+ // Normalize mixed
,
,
variations to
+ .replace(/<\s*br\s*\/?\s*>/gi, "
")
+ // Remove accidental duplicate
etc
+ .replace(/(
\s*){2,}/g, "
")
+ .trim();
+
return text;
}
+
const decodeUnicode = (str) => str.replace(/%u[0-9a-fA-F]{2,4}/g, (m) => String.fromCharCode(parseInt(m.slice(2), 16)));
- const version = '0.2.5';
+ const version = '0.2.6';
log('Supernotes v' + version + ' is ready! --offset ' + API_Meta.Supernotes.offset + 'To set the template of choice or to toggle the send to players option, Use the command !gmnote --config');
+//Changelong
+// 0.2.6 Reworked and updated Help system to use handout. Fixed logic issue Card output.
+// 0.2.5 fixed trailing space problem in command line, fixed linebreak issue.
+
+
+
on('chat:message', function(msg) {
if ('api' === msg.type && msg.content.match(/^!(gm|pc|self)note\b/)) {
@@ -465,7 +804,7 @@ sendChat ("notes","success. Virtual token id is " + virtualTokenID);
}
let secondOption = '';
- let args = msg.content.split(/\s+--/);
+ let args = msg.content.trim().split(/\s+--/);
let customTemplate = '';
let option = '';
@@ -634,7 +973,6 @@ whisper= whisper.replace(/<\/span>
/i,"")
handoutButton = ((undefined !== handoutButton) ? handoutButton.replace(/\[([^\]]*?)\]\(([^\)]*?)\)(?$1") : "");
whisper = ((whisper.length>0) ? "
" + whisper + "
" : "");
//log ("whisper = " + whisper);
-
return sendChat(whom, messagePrefix + '&{template:' + template + '}{{' + title + '=' + whom + '}} {{' + theText + '=' + message + whisper + playerButton + handoutButton + '}}');
}
@@ -871,11 +1209,10 @@ message = message.replace(/201px/,newHeight+'px');
}
} else {
if (option !== undefined && option.includes('help')) {
- message = 'Supernotes pulls the contents from a token's GM Notes field. If the token represents a character, you can optionally pull in the Bio or GM notes from the character, as well as the avatar, or extract just the image from the bio field. The user can decide whether to whisper the notes to the GM or broadcast them to all players. Finally, there is the option to add a footer to notes whispered to the GM. This footer creates a chat button to give the option of sending the notes on to the players.
This script as written is optimized for the D&D 5th Edition by Roll20 sheet, but can be adapted easily suing the Configuration section below.
Commands:!gmnote whispers the note to the GM
!pcnote sends the note to all players
Paramaters--token Pulls notes from the selected token's gm notes field. This is optional. If it is missing, the script assumes --token
--charnote Pulls notes from the gm notes field of the character assigned to a token.
--bio Pulls notes from the bio field of the character assigned to a token.
--avatar Pulls the image from the avatar field of the character assigned to a token.
--image Pulls first image from the bio field of the character assigned to a token, if any exists. Otherwise returns notice that no artwork is available
--images Pulls all images from the bio field of the character assigned to a token, if any exist. Otherwise returns notice that no artwork is available
--image[number] Pulls indexed image from the bio field of the character assigned to a token, if any exist. --image1 will pull the first image, --image2 the second and so on. Otherwise returns first image if available. If no images are available, returns notice that no artwork is available.
--template[templatename] Instead of using the configured sheet roll template, you can choose from between more than 10 custom templates that cover most common genres. Add the template command directly after the main prompt, followed by any of the regular parameters above. The current choices are:
generic. Just the facts, ma'am. Nothing fancy here.
dark. As above, but in reverse.
crt. Retro greenscreen for hacking and cyberpunk. Or for reports on that xenomorph hiding on your ship.
notebook. You know, for kids. Who like to ride bikes. Maybe they attend a school and fight vampires or rescue lost extraterrestrials
gothic. Classic noire horror for contending with Universal monsters or maybe contending with elder gods.
apoc. Messages scrawled on a wall. Crumbling and ancient, like the world that was.
scroll. High fantasy. Or low fantasy—we don't judge.
lcars. For opening hailing frequencies and to boldly split infinitives that no one has split before!
faraway. No animated title crawl, but still has that space wizard feel.
steam. Gears and brass have changed my life.
western. Return with us now to those thrilling days of yesteryear!
--help Displays help.
--config Returns a configuration dialog box that allows you to set which sheet's roll template to use, and to toggle the "Send to Players" footer.
ConfigurationWhen first installed, Supernotes is configured for the default roll template. It will display a config dialog box at startup that will allow you to choose a roll template based on your character sheet of choice, as well as the option to toggle whether you want the "Send to Players" footer button to appear.
You will need to edit the code of the script to create a custom configuration. The pre-installed sheets are:
Default Template
D&D 5th Edition by Roll20
5e Shaped
Pathfinder by Roll20
Pathfinder Community
Pathfinder 2e by Roll20
Starfinder
Call of Cthulhu 7th Edition by Roll20
';
- sendMessage('Supernotes', messagePrefix, template, title, theText, message, false);
-
+ buildSupernotesHelp();
+ return;
} else {
- if (!(option + '').match(/^(bio|charnote|tokenimage|tooltip|avatar|imag(e|es|e[1-9]))/)) {
+ if (!(option + '').match(/^(card|bio|charnote|tokenimage|tooltip|avatar|imag(e|es|e[1-9]))/)) {
option = 'token';
}
@@ -902,7 +1239,73 @@ message = message.replace(/201px/,newHeight+'px');
- if (option === 'tooltip') {
+if (option === 'card') {
+
+ (theToken || []).forEach(sel => {
+
+ const o = getObj('graphic', sel._id);
+ if (!o) return;
+
+ const tokenID = o.id;
+ const tokenName = o.get('name') || '';
+ const rawGM = o.get('gmnotes') || '';
+
+ // Always assign whom deterministically
+ whom = tokenName;
+
+ // Decode GM notes safely
+ let decodedGM = rawGM ? unescape(decodeUnicode(rawGM)) : '';
+
+ // Apply regex filtering if present
+ if (decodedGM && regex) {
+ decodedGM = _.filter(
+ decodedGM.split(/(?:[\n\r]+|
)/),
+ l => regex.test(l)
+ ).join('\r');
+ }
+
+ message = decodedGM || '';
+
+ // Crop GM-only content for player/self notes
+ if (command === '!pcnote' || command === '!selfnote') {
+ if (message.includes("-----")) {
+ message = message.split('-----')[0];
+ }
+ }
+
+ // Apply notitle
+ if (notitle) {
+ whom = '';
+ }
+
+ // Inject token image if message isn't an image URL
+ if (!/\.(png|jpg|jpeg|gif)/i.test(message)) {
+
+ let styledTokenImage = `

`;
+
+ if (!message) {
+ message = `
`;
+ }
+
+ message = styledTokenImage + message;
+ }
+
+ sendMessage(
+ whom,
+ messagePrefix,
+ template,
+ title,
+ theText,
+ message,
+ tokenID,
+ playerButton,
+ handoutButton
+ );
+
+ });
+
+ } else {
+ if (option === 'tooltip') {
(theToken || [])
.map(o => getObj('graphic', o._id))
.filter(g => undefined !== g)
@@ -1064,7 +1467,10 @@ message = message.replace(/201px/,newHeight+'px');
(theToken || [])
.map(o => getObj('graphic', o._id))
.filter(g => undefined !== g)
- .filter((o) => o.get('gmnotes').length > 0)
+ .filter((o) => {
+ const gm = (o && o.get) ? o.get('gmnotes') : '';
+ return !!(gm && gm.length > 0);
+})
.forEach(o => {
if (regex) {
message = _.filter(unescape(decodeUnicode(o.get('gmnotes'))).split(/(?:[\n\r]+|
)/), (l) => regex.test(l)).join('\r');
@@ -1101,6 +1507,7 @@ message = message.replace(/201px/,newHeight+'px');
].forEach(m => log(m));
*/
}
+ }
}
}
}
From 16b6e895eb271723178cafa726814f0c104c0d35 Mon Sep 17 00:00:00 2001
From: keithcurtis1
Date: Mon, 16 Feb 2026 13:46:50 -0800
Subject: [PATCH 06/10] Update print statement from 'Hello' to 'Goodbye'
---
Supernotes/0.2.6/Supernotes.js | 1520 ++++++++++++++++++++++++++++++++
1 file changed, 1520 insertions(+)
create mode 100644 Supernotes/0.2.6/Supernotes.js
diff --git a/Supernotes/0.2.6/Supernotes.js b/Supernotes/0.2.6/Supernotes.js
new file mode 100644
index 0000000000..a8b8bd6174
--- /dev/null
+++ b/Supernotes/0.2.6/Supernotes.js
@@ -0,0 +1,1520 @@
+var API_Meta = API_Meta||{};
+API_Meta.Supernotes={offset:Number.MAX_SAFE_INTEGER,lineCount:-1};
+{try{throw new Error('');}catch(e){API_Meta.Supernotes.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-3);}}
+
+
+// Supernotes_Templates can be called by other scripts. At this point ScriptCards is the only One Click script that does this.
+let Supernotes_Templates = {
+ generic: {
+ boxcode: ``,
+ titlecode: `
`,
+ textcode: "
",
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#ce0f69 !important; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color:#ce0f69; background-color: transparent;padding: 0px; border: none;'`,
+ buttondivider: ' | ',
+ handoutbuttonstyle: `style='display:inline-block; color:#ce0f69; background-color: transparent;padding: 0px; border: none;'`,
+ whisperStyle: `'background-color:#2b2130; color:#fbfcf0; display:block; border-width: 1px; border-style: solid; border-color:#a3a681; padding:5px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#bbb; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+ dark: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: "
",
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#a980bd; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color:#a980bd; background-color: transparent;padding: 0px; border: none;'`,
+ buttondivider: ' | ',
+ handoutbuttonstyle: `style='display:inline-block; color:#a980bd; background-color: transparent;padding: 0px; border: none;'`,
+ whisperStyle: `'background-color:#2b2130; color:#fbfcf0; display:block; border-width: 1px; border-style: solid; border-color:#a3a681; padding:5px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#bbb; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+
+ roll20dark: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: "
",
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#a980bd; font-weight:bold; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color:#fff; font-weight:bolder; background-color: #e7339d;border-radius: 4px; margin:4px; padding: 2px 6px 2px 6px; border: none; font-family:"proxima nova", sans-serif; ;'`,
+ buttondivider: '',
+ handoutbuttonstyle: `style='display:inline-block; color:#fff; font-weight:bolder; background-color: #e7339d;border-radius: 4px; margin:4px; padding: 2px 6px 2px 6px; border: none;font-family:"nunito black", nunito;'`,
+ whisperStyle: `'background-color:#f9cce7; color:#111; display:block; padding:5px; margin-top:20px;'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#702c91; font-weight:bold; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+ roll20light: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: "
",
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#702c91; font-weight:bold; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color:#fff; font-weight:bolder; background-color: #e7339d;border-radius: 4px; margin:4px; padding: 2px 6px 2px 6px; border: none; font-family:"proxima nova", sans-serif; ;'`,
+ buttondivider: '',
+ handoutbuttonstyle: `style='display:inline-block; color:#fff; font-weight:bolder; background-color: #e7339d;border-radius: 4px; margin:4px; padding: 2px 6px 2px 6px; border: none; font-family:"Nunito Black", nunito;'`,
+ whisperStyle: `'background-color:#f9cce7; color:#111; display:block; padding:5px; margin-top:20px;'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#702c91; font-weight:bold; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+
+ lcars: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: "
",
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#cc6060; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; border:none; color:black; background-color: #cc6060; border-radius: 10px 0px 0px 10px; padding: 2px 4px 2px 4px;margin-top: 12px; font-size: 10px; font-family: Tahoma, sans-serif; font-stretch: condensed !important; text-transform: uppercase;'`,
+ buttondivider: '',
+ handoutbuttonstyle: `style='display:inline-block; border:none; color:black; background-color: #cc6060; border-radius: 0px 10px 10px 0px; padding: 2px 4px 2px 4px;margin-top: 12px; margin-left:4px; font-size: 10px; font-family: Tahoma, sans-serif; font-stretch: condensed !important; text-transform: uppercase;'`,
+ whisperStyle: `'border-radius: 10px 0px 0px 10px; color:#ffae21; border-color: #ffae21; display:block; border-width: 0px 0px 5px 15px; border-style: solid; padding:5px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#cc6060; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+ faraway: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: "
",
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#13f2fc; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color:#13f2fc; font-weight:normal; background-color: transparent;padding: 0px; border: none;'`,
+ buttondivider: `
• `,
+ handoutbuttonstyle: `style='display:inline-block; color:#13f2fc; font-weight:normal; background-color: transparent; padding: 0px; border: none;'`,
+ whisperStyle: `'background-color:transparent; color:#feda4a; display:block; border-width: 8px; border-style: solid; border-radius:5px; border-color:#feda4a; padding:15px; margin-top:10px;'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#13f2fc; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+ strange: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: "
",
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#ff1515; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color:#ff1515; font-family: "Goblin One"; font-weight:normal; font-size: 10px; background-color: transparent;padding: 0px; border: none;'`,
+ buttondivider: `
• `,
+ handoutbuttonstyle: `style='display:inline-block; color:#ff1515; font-family: "Goblin One"; font-weight:normal; font-size: 10px; background-color: transparent; padding: 0px; border: none;'`,
+ whisperStyle: `'background-color:##4f0606; color:#ff1515; display:block; border: 1px solid #000; box-shadow: 0 0 5px #ff1515; padding:5px; margin-top:10px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#bbb; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+ gothic: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: `

`,
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#ccc; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color:#ccc; font-size:12px; font-weight:normal; background-color: transparent;padding: 0px; border: none;'`,
+ buttondivider: `

`,
+ handoutbuttonstyle: `style='display:inline-block; color:#ccc; font-size:12px; font-weight:normal; background-color: transparent; padding: 0px; border: none;'`,
+ whisperStyle: `'background-color:#2b2130; color:#ddd; display:block; border-width: 1px; border-style: solid; border-color:#a3a681; padding:5px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#aaa; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+ western: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: `

`,
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#000; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color:#7e2d40; background-color: transparent;padding: 0px; border: none'`,
+ buttondivider: `

`,
+ handoutbuttonstyle: `style='display:inline-block; color:#7e2d40; background-color: transparent;padding: 0px; border: none'`,
+ whisperStyle: `'background-color:#382d1d; color:#ebcfa9; font-style: italic; display:block; border-width: 1px; border-style: solid; border-color:#a3a681; padding:5px; margin-top:5px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#fabe69; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+ dragon: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: `
`,
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#0e3365; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color: #0e3365; font-size:14px; background-color: transparent;padding: 0px; border: none'`,
+ buttondivider: " • ", //`

`,
+ handoutbuttonstyle: `style='display:inline-block; color: #0e3365; font-size:14px; background-color: transparent;padding: 0px; border: none'`,
+ whisperStyle: `'display:block; border-width: 5px 0px 5px 0px; border-style: solid; border-color:#58170D; padding:5px; margin-top:9px;'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#0e3365; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+
+
+ wizard: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: `
`,
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#58170D; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color: #000; font-size:12px; background-color: transparent;padding: 0px; border: none'`,
+ buttondivider: " • ", //`

`,
+ handoutbuttonstyle: `style='display:inline-block; color: #000; font-size:12px; background-color: transparent;padding: 0px; border: none'`,
+ whisperStyle: `'background-color:#E0E5C1; color:#000; display:block; border-width: 1px; border-width: 1px 0px 1px 0px; border-style: solid; border-color:#58170D; padding:5px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#58170D; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+path: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: `
`,
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#5e0000; font-weight:bold; background-color: transparent; padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color: #eee; font-size:12px; background-color: #5e0000; padding: 0px 4px 0px 4px; border-style:solid; border-width: 2px 4px 2px 4px; border-color: #d9c484; text-transformation: all-caps; font-family: "gin", impact, "Arial Bold Condensed", sans-serif;'`,
+ buttondivider: " ", //`

`,
+ handoutbuttonstyle: `style='display:inline-block; color: #eee; font-size:12px; background-color: #5e0000; padding: 0px 4px 0px 4px; border-style:solid; border-width: 2px 4px 2px 4px; border-color: #d9c484; text-transformation: all-caps; font-family: "gin", impact, "Arial Bold Condensed", sans-serif;'`,
+ whisperStyle: `'background-color:#dbd1bc; color:#000; display:block; border-width: 1px; margin-top:15px; padding:5px; font-size: 15px; font-family: "Good OT", arial, sans-serif;'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#58170D; background-color: transparent; font-weight:bold; padding: 0px; border: none'`,
+ footer: ""
+},
+
+apoc: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: `
`,
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#555; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color:#000; font-size:14px; font-weight:normal; background-color: transparent;padding: 0px; border: none;'`,
+ buttondivider: " / ",
+ handoutbuttonstyle: `style='display:inline-block; color:#000; font-size:14px; font-weight:normal; background-color: transparent; padding: 0px; border: none;'`,
+ whisperStyle: `'background-color:#403f3d; color:#ddd; display:block; padding:5px !important; margin:5px; font-family: "Shadows Into Light", Monaco,"Courier New", monospace !important; '`,
+ whisperbuttonstyle: `style='display:inline-block; color:#bbb; background-color: transparent;padding: 0px; border: none'`,
+ footer: `

`
+ },
+
+ roman: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: `
`,
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#7c6f39; font-weight: bold; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color:#000; font-size:12px; font-weight:normal; background-color: transparent;padding: 0px; border: none;'`,
+ buttondivider: " | ",
+ handoutbuttonstyle: `style='display:inline-block; color:#000; font-size:12px; font-weight:normal; background-color: transparent; padding: 0px; border: none;'`,
+ whisperStyle: `'background-image: url(https://files.d20.io/images/459209597/cdZeKGAy2_NKcU1Wjkjeew/original.jpg); background-repeat: no-repeat; background-size: 100% 100%; background-color:#403f3d; color:#ddd; display:block; padding:8px !important; margin:5px 0px; text-shadow: none; line-height:16px;'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#bbaa55; font-weight: bolder !important; background-color: transparent;padding: 0px; border: none'`,
+ footer: `

`
+ },
+
+ notebook: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: `
`,
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color: red; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color:red; font-size:10px; font-weight:normal; background-color: transparent;padding: 0px; border: none;'`,
+ buttondivider: `
/`,
+ handoutbuttonstyle: `style='display:inline-block; color:red; font-size:10px; font-weight:normal; background-color: transparent; padding: 0px; border: none;'`,
+ whisperStyle: `'color:red; display:block; padding-top:7px; font-family: "Patrick Hand", Monaco,"Courier New", monospace; line-height: 16px;'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#333; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+ steam: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: "
",
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#056b20; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color:#056b20; font-size:12px; font-weight:normal; background-color: transparent;padding: 0px; border: none;'`,
+ buttondivider: `

`,
+ handoutbuttonstyle: `style='display:inline-block; color:#056b20; font-size:12px; font-weight:normal; background-color: transparent; padding: 0px; border: none;'`,
+ whisperStyle: `'background-color:#2b2130; color:#fbfcf0; display:block; border-width: 1px; border-style: solid; border-color:#a3a681; padding:5px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#fff; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+ treasure: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: "
",
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#8a4100; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color:#634401; font-size:14px; font-weight:normal; background-color: transparent;padding: 0px; border: none;'`,
+ buttondivider: `

`,
+ handoutbuttonstyle: `style='display:inline-block; color:#401e00; font-size:14px; font-weight:normal; background-color: transparent; padding: 0px; border: none;'`,
+ whisperStyle: `'background-color:#401e00; color:#eee; font-family: Tahoma, serif; display:block; border-width: 1px; border-style: solid; border-color:#a3a681; margin-top:10px;padding:5px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#e3b76f; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+choices: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: `
`,
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#eee; hover: yellow; background-color: transparent;padding: 0px; border: none; '`,
+ playerbuttonstyle: `style='display:inline-block; color: #eee; font-size:16px; font-family: "Minion", "Minion Pro", serif; background-color: transparent;padding: 0px; border: none'`,
+ buttondivider: " ◼ ", //`

`,
+ handoutbuttonstyle: `style='display:inline-block; color: #eee; font-size:16px; font-family: "Minion", "Minion Pro", serif; background-color: transparent;padding: 0px; border: none'`,
+ whisperStyle: `'background-image: linear-gradient(to bottom,#4b443d,#3f3732,#4b443d); background-color: transparent; color:#f8e8a6; display:block; border-width: 1px; border: 1px solid #4f4841; margin: 20px, -12px, 15px, -12px; padding:10px, 10px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#eee; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+},
+gate3: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: `
`,
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#eada8d; background-color: transparent;padding: 0px; border: none; '`,
+ playerbuttonstyle: `style='display:inline-block; color: #eee; font-size:16px; font-family: "Minion", "Minion Pro", serif; background-color: transparent;padding: 0px; border: none'`,
+ buttondivider: " ◼ ", //`

`,
+ handoutbuttonstyle: `style='display:inline-block; color: #eee; font-size:16px; font-family: "Minion", "Minion Pro", serif; background-color: transparent;padding: 0px; border: none'`,
+ whisperStyle: `'background-image: linear-gradient(to bottom,#4b443d,#3f3732,#4b443d); background-color: transparent; color:#f8e8a6; display:block; border-width: 1px; border: 1px solid #4f4841; margin: 20px, -12px, 15px, -12px; padding:10px, 10px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#eee; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+},
+
+
+ crt: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: "
",
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#fff; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block;font-weight:bold; color:white; background-color: transparent;padding: 0px; border: none;font-size: 12px'`,
+ buttondivider: '|',
+ handoutbuttonstyle: `style='display:inline-block;font-weight:bold; color:white; background-color: transparent;padding: 0px; border: none;font-size: 12px'`,
+ whisperStyle: `'background-color:#2b2130; color:#fbfcf0; display:block; border-width: 1px; border-style: solid; border-color:#a3a681; padding:5px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#fff; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+ news: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: `
`,
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#222; text-decoration:underline; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block;float:right; margin-top:5px; font-weight:bold; color:#444; background-color: transparent;padding: 0px; border: none;font-size: 12px'`,
+ buttondivider: ' ',
+ handoutbuttonstyle: `style='display:inline-block;float:left; margin-top:5px; font-weight:bold; color:#444; background-color: transparent;padding: 0px; border: none;font-size: 12px'`,
+ whisperStyle: `'background-color: rgba(0, 0, 0, 0.1); color:#444; font-size: 14px;font-family: arial, helvetica, sans-serif; padding:8px; display:block; border: 1px solid #444;'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#444; text-decoration:underline; background-color: transparent; padding: 0px; border: none'`,
+ footer: ""
+ },
+
+ scroll: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: `
`,
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#7e2d40; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; color:#7e2d40; background-color: transparent;padding: 0px; border: none'`,
+ buttondivider: ' | ',
+ handoutbuttonstyle: `style='display:inline-block; color:#7e2d40; background-color: transparent;padding: 0px; border: none'`,
+ whisperStyle: `'background-color:#58170d; color:#d9bf93; display:block; padding:5px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#fce5bb; background-color: transparent;padding: 0px; border: none'`,
+ footer: ""
+ },
+
+ scroll2: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: `
`,
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#58170D; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; font-size: 14px !important; color:#58170D; background-color: transparent;padding: 0px; border: none'`,
+ buttondivider: ' | ',
+ handoutbuttonstyle: `style='display:inline-block; font-size: 14px !important; color:#58170D; background-color: transparent;padding: 0px; border: none'`,
+ whisperStyle: `'background-color:#241605; color:#eee; box-shadow: 0px 0px 5px 5px #241605; display:block; border-radius:15px; padding:5px; margin: 15px 5px 10px 5px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#fcdd6d; background-color: transparent;padding: 0px; border: none'`,
+ footer: `

`
+ },
+
+ vault: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: `
`,
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; color:#111; text-decoration: underline; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; font-size: 15px !important; color:#fef265; text-shadow: 2px 2px 2px #111; background-color: transparent;padding: 0px; border: none'`,
+ buttondivider: `

`,
+ handoutbuttonstyle: `style='display:inline-block; font-size: 15px !important; color:#fef265; text-shadow: 2px 2px 2px #111;background-color: transparent;padding: 0px; border: none'`,
+ whisperStyle: `'background-color: #transparent; background-image: url(https://files.d20.io/images/459209469/UA2E7Vyf-kncA8k1jUuyAg/original.png; color:#111; display:block; text-shadow: none; text-align:center; font-family: "Contrail One"; border-radius:3px; padding:5px; margin: 15px -20px 10px -20px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#284a73; background-color: transparent;padding: 0px; border: none'`,
+ footer: ``
+ },
+
+ osrblue: {
+ boxcode: `
`,
+ titlecode: `
`,
+ textcode: `
`,
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block !important; color:#333; text-decoration: underline; background-color: transparent;padding: 0px; border: none'`,
+ playerbuttonstyle: `style='display:inline-block; font-size: 14px !important; color:#333; text-decoration: underline; background-color: transparent;padding: 0px; border: none'`,
+ buttondivider: `|`,
+ handoutbuttonstyle: `style='display:inline-block; font-size: 14px !important; color:#333; text-decoration: underline; background-color: transparent;padding: 0px; border: none'`,
+ whisperStyle: `'background-color: #729aa5; color:#eee; display:block; text-align:center; font-family: "Arial"; padding:5px; margin: 15px -20px 10px -20px'`,
+ whisperbuttonstyle: `style='display:inline-block; color:#eee; text-decoration: underline; background-color: transparent;padding: 0px; border: none'`,
+ footer: ``
+ }
+
+};
+
+on('ready', function() {
+ if (!_.has(state, 'Supernotes')) {
+ state.Supernotes = {
+ sheet: 'Default',
+ template: 'default',
+ title: 'name',
+ theText: '',
+ sendToPlayers: true,
+ makeHandout: true,
+ darkMode: false
+ };
+ message = 'Welcome to Supernotes! If this is your first time running it, the script is set to use the Default Roll Template. You can choose a different sheet template below, as well as decide whether you want the script to display a "Send to Players" footer at the end of every GM message. It is currently set to true.
[Default Template - any sheet](!gmnote --config|default)
[D&D 5th Edition by Roll20](!gmnote --config|dnd5e)
[DnD 5e Shaped](!gmnote --config|5eshaped)
[Pathfinder by Roll20](!gmnote --config|pfofficial)
[Pathfinder Community](!gmnote --config|pfcommunity)
[Pathfinder 2e by Roll20](!gmnote --config|pf2e)
[Starfinder by Roll20](!gmnote --config|starfinder)
[Call of Cthulhu 7th Edition by Roll20](!gmnote --config|callofcthulhu)
[Toggle Send to Players](!gmnote --config|sendtoPlayers)';
+ sendChat('Supernotes', '/w gm &{template:' + state.Supernotes.template + '}{{' + state.Supernotes.title + '=' + 'Config' + '}} {{' + state.Supernotes.theText + '=' + message + '}}');
+ }
+});
+
+on('ready', () => {
+
+
+ /* =========================================================
+ * Supernotes Help Handout Builder
+ * ========================================================= */
+
+const buildSupernotesHelp = () => {
+
+ const HANDOUT_NAME = "Help: Supernotes";
+ const HANDOUT_AVATAR = "https://files.d20.io/images/470559564/QxDbBYEhr6jLMSpm0x42lg/original.png?1767857147"; // change if desired
+
+const helpHtml = `
+
Supernotes
+
Documentation for v.${version}
+
+
+
+
Overview
+
+
+Supernotes pulls content from a token’s GM Notes field and from other character fields not normally accessible to macros.
+If a token represents a character, you may retrieve:
+
+
+
+- Character GM Notes
+- Character Bio
+- Character Avatar
+- Bio images (single, indexed, or all)
+- Token tooltip
+- Token image
+
+
+
+Notes may be whispered to the GM, sent to all players, whispered to the sender, or written directly to a named handout.
+A footer button may optionally appear on GM whispers, allowing the note to be forwarded to players.
+
+
+
+Images, API command buttons, links, markdown image syntax [x](imageURL), and most special characters pass through correctly in both chat and handouts.
+
+
+
+
Special Control Character for Inline GMnotes
+
-----
+
Five dashes placed in the gmnotes of a token indicate that any following content is trested as gm-only text when sent to chat.
+
+
+
+
+
Commands
+
+
!gmnote
+Whispers note to GM.
+
+
!pcnote
+Sends note to all players.
+
+
!selfnote
+Whispers note to the command sender.
+
+
+
+
Parameters
+
+
Sources
+
+- --token
+Pull from selected token GM Notes (default). Token does not require a character.
+
+- --charnote
+Pull from represented character GM Notes.
+
+- --bio
+Pull from character Bio field.
+
+- --avatar
+Return character Avatar image.
+
+- --image
+Return first Bio image.
+
+- --images
+Return all Bio images.
+
+- --image[number]
+Return indexed Bio image (e.g. --image1, --image2).
+
+- --tooltip
+Return selected token tooltip.
+
+- --tokenimage
+Return selected token image.
+
+- --card
+Return token image and gmnotes in one report.
+
+
+
+
Options
+
+
+- --notitle
+Suppress title in chat output. May be added to any command in any order.
+
+- --idTOKENID
+Read notes from specific token ID. No space after --id. Example:
!gmnote --id-1234567890abcdef
+
+- --handout|Handout Name|
+Send output to named handout instead of chat.
+Creates the handout if it does not exist.
+Content above the automatic horizontal rule remains persistent.
+
+
+- --help
+Displays help.
+
+- --config
+Opens configuration dialog.
+
+
+
+
+
Examples
+
+
!pcnote --bio
+
Sends selected character Bio to all players.
+
+
!gmnote --charnote
+
Whispers character GM Notes to GM.
+
+
!pcnote --image --notitle
+
Sends first image without revealing title.
+
+
+
+
Templates
+
+
+Add a template using:
+
+
+
--template|templatename
+
+
+Example:
+
+
+
!gmnote --template|crt
+!pcnote --template|notebook --bio
+!pcnote --template|faraway --tokenimage
+
+
+All templates include inline buttons and support Send to Players and Make Handout.
+Handouts use Roll20’s native styling for cross-platform reliability.
+
+
+
+
+
Available Templates
+
+
+
+ generic Minimal presentation |
+ dark Inverted generic |
+ crt Retro greenscreen |
+
+
+
+ notebook Modern supernatural |
+ gothic Noir horror |
+ apoc Post-apocalyptic |
+
+
+
+ scroll Fantasy parchment |
+ scroll2 Even scrollier |
+ lcars Starship interface |
+
+
+
+ faraway Space opera |
+ steam Steampunk brass |
+ western Frontier theme |
+
+
+
+ dragon Three-fivey style |
+ wizard Fifth edition flavor |
+ strange Bikes & mystery |
+
+
+
+ gate3 GOTY-inspired |
+ choices Alternate GOTY style |
+ roll20light Authority feel |
+
+
+
+ roll20dark Authority, darker |
+ news Bleeding newsprint |
+ treasure Loot listing |
+
+
+
+ vault Comforting shelter |
+ path PF2 Adventure Path |
+ osrblue Yellowed graph paper |
+
+
+
+ roman Bonus classical style |
+ |
+ |
+
+
+
+
+
+
Script Synergy
+
+
+Templates are exposed under the object Supernotes_Templates for use in other scripts such as ScriptCards and PinNote .
+Supernotes also integrates amny of it functions with Reporter .
+
+
+For the convenience of coders who wish to integrate their scripts to use Supernotes, the CSS for the tempaltes is exposed to the global space as Supernotes_Templates.templatename. These can be found at the top of the script. Contact keithcurtis if anything is unclear.
+
+
+
+
+
+
Configuration
+
+
+On installation, Supernotes defaults to the Default roll template.
+The configuration dialog allows you to:
+
+
+
+- Select a sheet roll template
+- Toggle the “Send to Players” footer button
+
+
+
+Supported sheet templates include:
+
+
+
+- Default Template
+- D&D 5th Edition by Roll20
+- 5e Shaped
+- Pathfinder by Roll20
+- Pathfinder Community
+- Pathfinder 2e by Roll20
+- Starfinder
+
+
+
+
+
Troubleshooting
+
+
+If you experience template issues or configuration problems, you may use the buttons below to restore default behavior or re-open the configuration dialog.
+
+
+
+
+
+Restore Default Template resets Supernotes to the Default roll template.
+Re-Run Configuration opens the configuration dialog to select a sheet template and toggle footer options.
+
+
+`;
+
+
+ // Find existing handout
+ let handout = findObjs({
+ _type: "handout",
+ name: HANDOUT_NAME
+ })[0];
+
+ // Create if missing
+ if (!handout) {
+ handout = createObj("handout", {
+ name: HANDOUT_NAME,
+ archived: false
+ });
+ }
+
+ // Always overwrite content + avatar
+ handout.set({
+ notes: helpHtml,
+ avatar: HANDOUT_AVATAR
+ });
+
+ const link = `http://journal.roll20.net/handout/${handout.get("_id")}`;
+
+ const box =
+ `
`;
+
+ sendChat("Supernotes", `/w gm ${box}`, null, { noarchive: true });
+};
+
+
+ function parseMarkdown(markdownText) {
+ const htmlText = markdownText
+ .replace(/^### (.*$)/gim, '
$1
')
+ .replace(/^## (.*$)/gim, '
$1
')
+ .replace(/^# (.*$)/gim, '
$1
')
+ .replace(/^\> (.*$)/gim, '
$1
')
+ .replace(/\*\*(.*)\*\*/gim, '
$1')
+ .replace(/\*(.*)\*/gim, '
$1')
+ .replace(/!\[(.*?)\]\((.*?)\)/gim, "

")
+ .replace(/\[(.*?)\]\((.*?)\)/gim, "
$1")
+ .replace(/\n$/gim, '
')
+
+ return htmlText.trim()
+ }
+
+function cleanText(text,buttonStyle){
+ text = ((undefined !== text) ? text.replace(/\[([^\]]*?)\]\(([^\)]*?)\)(?$1").replace(/
/gm, "").replace(/<\/p>/gm, "
").replace("padding:5px'>
", "padding:5px'>") : "");
+ text = text.replace('
+ .replace(/\r?\n+/g, "
")
+ // Normalize mixed
,
,
variations to
+ .replace(/<\s*br\s*\/?\s*>/gi, "
")
+ // Remove accidental duplicate
etc
+ .replace(/(
\s*){2,}/g, "
")
+ .trim();
+
+return text;
+}
+
+
+
+ const decodeUnicode = (str) => str.replace(/%u[0-9a-fA-F]{2,4}/g, (m) => String.fromCharCode(parseInt(m.slice(2), 16)));
+
+ const version = '0.2.6';
+ log('Supernotes v' + version + ' is ready! --offset ' + API_Meta.Supernotes.offset + 'To set the template of choice or to toggle the send to players option, Use the command !gmnote --config');
+//Changelong
+// 0.2.6 Reworked and updated Help system to use handout. Fixed logic issue Card output.
+// 0.2.5 fixed trailing space problem in command line, fixed linebreak issue.
+
+
+
+
+ on('chat:message', function(msg) {
+ if ('api' === msg.type && msg.content.match(/^!(gm|pc|self)note\b/)) {
+ let match = msg.content.match(/^!gmnote-(.*)$/);
+let selectedObject = msg.selected;
+
+//################## EXPERIMENTAL TO GET TOKEN ID FROM SUPPLIED VALUE
+if(msg.content.includes("--token|")){
+ virtualTokenID = msg.content.split(/--token\|/)[1].split(/\s/)[0];
+sendChat ("notes","success. Virtual token id is " + virtualTokenID);
+ if (virtualTokenID.length !== 20 && virtualTokenID.charAt(0) !== "-"){
+ sendChat ("notes","this is not a token id :" + virtualTokenID);
+ sendChat ("notes","player page id :" + Campaign().get("playerpageid"));
+
+ selectedObject = findObjs({
+ _type: "graphic",
+ _id: virtualTokenID,
+ });
+ log ("selectedObject is " + selectedObject);
+ // selectedObject = theToken[0];
+ }
+ if (selectedObject){
+ sendChat ("notes", "number of 'selected' objects is " +selectedObject.length);
+ } else{
+ sendChat ("notes", "no passed value");
+ }
+//sendChat ("notes","virtual ID is " + selectedObject[0].get("_id"));
+}
+//################## EXPERIMENTAL TO GET TOKEN ID FROM SUPPLIED VALUE
+
+
+
+
+
+
+
+ //define command
+ let command = msg.content.split(/\s+--/)[0];
+ let sender = msg.who;
+ let senderID = msg.playerid;
+
+ let isGM = playerIsGM(senderID);
+ let messagePrefix = '/w gm ';
+ if (command === '!pcnote') {
+ messagePrefix = '';
+ }
+
+ if (command === '!selfnote') {
+ messagePrefix = '/w ' + sender + ' ';
+ }
+
+ let secondOption = '';
+ let args = msg.content.trim().split(/\s+--/);
+
+ let customTemplate = '';
+ let option = '';
+ let notitle = false;
+ let id = '';
+ let tokenImage = '';
+ let tooltip = '';
+ let tokenName = '';
+ let trueToken = [];
+ let tokenID = '';
+ let handoutTitle = '';
+ let whisper = '';
+
+ let templates = Supernotes_Templates;
+
+
+
+
+ function sendMessage(whom, messagePrefix, template, title, theText, message, tokenID, playerButton, handoutButton) {
+ handoutButton = ((handoutButton) ? handoutButton.replace(/NamePlaceholder/, whom) : handoutButton);
+
+ if (message === "" && option.match(/^(bio|charnote|token|tooltip)/)) {
+ message = `The information does not exist for the ${option} option`
+ }
+
+ if (handoutTitle === '') {
+ //Crops out GM info on player messages
+ if (isGM) {
+ //message = (message.includes("-----") ? message.split('-----')[0] + "" + message.split('-----')[1] + "
" : message);
+ whisper = (message.includes("-----") ? message.split('-----')[1] : "");
+ message = (message.includes("-----") ? message.split('-----')[0] : message);
+
+ }
+
+ if (customTemplate.length > 0) {
+ let chosenTemplate = templates.generic;
+ switch (customTemplate) {
+ case "crt":
+ chosenTemplate = templates.crt;
+ break;
+ case "dark":
+ chosenTemplate = templates.dark;
+ break;
+ case "roll20light":
+ chosenTemplate = templates.roll20light;
+ break;
+ case "roll20dark":
+ chosenTemplate = templates.roll20dark;
+ break;
+ case "scroll":
+ chosenTemplate = templates.scroll;
+ break;
+ case "scroll2":
+ chosenTemplate = templates.scroll2;
+ break;
+ case "vault":
+ chosenTemplate = templates.vault;
+ break;
+ case "osrblue":
+ chosenTemplate = templates.osrblue;
+ break;
+ case "lcars":
+ chosenTemplate = templates.lcars;
+ break;
+ case "faraway":
+ chosenTemplate = templates.faraway;
+ break;
+ case "strange":
+ chosenTemplate = templates.strange;
+ break;
+ case "gothic":
+ chosenTemplate = templates.gothic;
+ break;
+ case "western":
+ chosenTemplate = templates.western;
+ break;
+ case "dragon":
+ chosenTemplate = templates.dragon;
+ break;
+ case "wizard":
+ chosenTemplate = templates.wizard;
+ break;
+ case "path":
+ chosenTemplate = templates.path;
+ break;
+ case "treasure":
+ chosenTemplate = templates.treasure;
+ break;
+ case "steam":
+ chosenTemplate = templates.steam;
+ break;
+ case "gate3":
+ chosenTemplate = templates.gate3;
+ break;
+ case "choices":
+ chosenTemplate = templates.choices;
+ break;
+ case "apoc":
+ chosenTemplate = templates.apoc;
+ break;
+ case "news":
+ chosenTemplate = templates.news;
+ break;
+ case "roman":
+ chosenTemplate = templates.roman;
+ break;
+ case "notebook":
+ chosenTemplate = templates.notebook;
+ break;
+ case "bob":
+ break;
+ default:
+ chosenTemplate = templates.generic;
+ // code block
+ }
+
+
+
+
+ playerButton = playerButton.split('\n')[1];
+
+ playerButton = ((undefined !== playerButton) ? playerButton.replace(/\[(.*?)\]\((.*?)\)/gim, "$1") : "");
+ handoutButton = ((undefined !== handoutButton) ? handoutButton.replace(/\[(.*?)\]\((.*?)\)/gim, "
$1").replace(" |
0) ? "" + whisper + "
" : "");
+
+
+message = cleanText(message,chosenTemplate.buttonstyle);
+//the following lines attempt to account for numerous Roll20 CSS and HTML oddities.
+whisper = cleanText(whisper,chosenTemplate.whisperbuttonstyle);
+whisper= whisper.replace(/<\/span>
/i,"")
+.replace(/
/i,'')
+.replace(/
/i,'
')
+.replace(/(
|<\/p>)/,'')
+.replace(/>
/i,'>');
+
+
+
+
+
+
+// message = ((undefined !== message) ? message.replace(/\[([^\]]*?)\]\(([^\)]*?)\)(?$1
").replace(/
/gm, "").replace(/<\/p>/gm, "
").replace("padding:5px'>
' + chosenTemplate.footer + '
');
+ }
+
+
+
+ } else {
+ playerButton = ((undefined !== playerButton) ? playerButton.replace(/\[([^\]]*?)\]\(([^\)]*?)\)(?$1") : "");
+ handoutButton = ((undefined !== handoutButton) ? handoutButton.replace(/\[([^\]]*?)\]\(([^\)]*?)\)(?$1") : "");
+whisper = ((whisper.length>0) ? "
" + whisper + "
" : "");
+//log ("whisper = " + whisper);
+ return sendChat(whom, messagePrefix + '&{template:' + template + '}{{' + title + '=' + whom + '}} {{' + theText + '=' + message + whisper + playerButton + handoutButton + '}}');
+ }
+
+ } else {
+ let noteHandout = findObjs({
+ type: 'handout',
+ name: handoutTitle
+ });
+ noteHandout = noteHandout ? noteHandout[0] : undefined;
+
+ if (!noteHandout) {
+ noteHandout = createObj('handout', {
+ name: handoutTitle,
+ archived: false,
+ inplayerjournals: "",
+ controlledby: ""
+ });
+ let noteHandoutid = noteHandout.get("_id");
+ sendChat('Supernotes', `/w gm Supernotes has created a handout named
${handoutTitle}.
Click
here to open.`, null, {
+ noarchive: true
+ });
+ }
+ if (noteHandout) {
+
+ playerButton = '
Send to Players in Chat';
+ if (makeHandout) {
+ handoutButton = ((playerButton) ? ' | ' : '
') + '
Make Handout';
+ }
+ message = message.replace(/\[.*?\]\((.*?\.(jpg|jpeg|png|gif))\)/g, `

`);
+ message = message.replace(/\[(.*?)\]\((.*?)\)/g, '
$1');
+ message = message.replace(/
![]()
/g, `
![]()
\(\d*\)/)) {
+ let reportCount= notes.match(/(?<=
\()\d+/);;
+//log ("reportCount = " + reportCount);
+
+let newHeight = reportCount * 20;
+if (newHeight > 500){newHeight = 500};
+if (newHeight < 200){newHeight = 200};
+//log ("newHeight = " + newHeight);
+message = message.replace(/201px/,newHeight+'px');
+
+ }
+//##############TEST FOR VARIABLE IMAGE HEIGHT BASED ON HEIGHT OF REPORT###################################################
+
+
+ if (notes.includes('')) {
+ if (notes.includes('!report')) {
+ notes = notes.split('')[0] + '';
+ } else {
+ notes = notes.split(/
/i)[0] + '';
+ }
+ } else {
+ playerButton = '';
+ handoutButton = '';
+ notes = ''; //';
+ }
+ /*if (notes.includes(' ')) {
+ notes = notes.split(' ')[0] + ' '
+ } else {
+ notes = ' '
+ }*/
+ //message = '' + message +'
';
+
+ noteHandout.set("gmnotes", gmnote);
+ noteHandout.set("notes", notes + "" + whom + "
" + message + playerButton + handoutButton)
+ //THIS NEEDS A TOGGLE
+ //if(!tokenImage.includes("marketplace")){noteHandout.set("avatar", tokenImage+"?12345678")}
+ })
+ } else {
+ sendChat('Supernotes', whom + `No handout named ${handoutTitle} was found.`, null, {
+ noarchive: true
+ }, )
+ }
+
+ }
+
+ }
+
+ let theToken = selectedObject;
+
+ args.forEach(a => {
+ if (a === 'notitle') {
+ notitle = true
+ }
+ if (a.includes('id-')) {
+ id = a.split(/id/)[1]
+ }
+ if (a.match(/handout\|.*?\|/)) {
+ handoutTitle = a.match(/handout\|.*?\|/).toString().split('|')[1]
+ }
+ if (a !== command && !(a.includes('id-')) && !(a.includes('handout|')) && a !== 'notitle') {
+ option = a
+ }
+ if (a.includes('template|')) {
+ customTemplate = a.split(/\|/)[1]
+ }
+
+ });
+
+ ((id) ? theToken = [{
+ "_id": id,
+ "type": "graphic"
+ }] : theToken = selectedObject);
+
+
+ if (undefined !== theToken) {
+ trueToken = getObj('graphic', theToken[0]._id);
+ tokenImage = trueToken.get('imgsrc');
+ tokenTooltip = trueToken.get('tooltip');
+ tokenName = trueToken.get('name');
+ tokenID = trueToken.get('_id');
+ }
+
+
+
+ const template = state.Supernotes.template;
+ const title = state.Supernotes.title;
+ const theText = state.Supernotes.theText;
+ const sendToPlayers = state.Supernotes.sendToPlayers;
+ const makeHandout = state.Supernotes.makeHandout || false;
+ const darkMode = state.Supernotes.darkMode || false;
+ const whisperStyle = ((darkMode) ? `'background-color:#2b2130; color:#fbfcf0; display:block; border-width: 1px; border-style: solid; border-color:#a3a681; padding:5px'` : `'background-color:#fff; color:#000; display:block; border-width: 1px; border-style: solid; border-color:#a3a681; padding:5px'`);
+
+ const whisperColor = ((darkMode) ? "#2b2130" : "#fbfcf0");
+ const whisperTextColor = ((darkMode) ? "#fff" : "#000");
+ const buttonstyle = ((darkMode) ? `style='display:inline-block; color:#a980bd; font-size: 0.9em; background-color: transparent;padding: 0px; border: none'` : `style='display:inline-block; color:#ce0f69; font-size: 0.9em; background-color: transparent;padding: 0px; border: none'`);
+
+
+
+
+ if (option !== undefined && option.includes('config')) {
+ let templateChoice = option.split('|')[1]
+
+ if (templateChoice === undefined) {
+ message = 'Current sheet template:
' + state.Supernotes.sheet + '
Send to Players:
' + state.Supernotes.sendToPlayers + '
Choose a template for Supernotes to use.
[Default Template - any sheet](!gmnote --config|default)
[D&D 5th Edition by Roll20](!gmnote --config|dnd5e)
[DnD 5e Shaped](!gmnote --config|5eshaped)
[Pathfinder Community](!gmnote --config|pfcommunity)
[Pathfinder by Roll20](!gmnote --config|pfofficial)
[Pathfinder 2e by Roll20](!gmnote --config|pf2e)
[Starfinder by Roll20](!gmnote --config|starfinder)
[Call of Cthulhu 7th Edition by Roll20](!gmnote --config|callofcthulhu)
[Toggle Send to Players](!gmnote --config|sendtoPlayers)
[Toggle Make Handout button](!gmnote --config|makeHandout)
[Toggle Darkmode](!gmnote --config|darkMode)'
+ sendChat('Supernotes', messagePrefix + '&{template:' + template + '}{{' + title + '=' + 'Config' + '}} {{' + theText + '=' + message + '}}');
+ }
+
+
+ switch (templateChoice) {
+ case 'default':
+ state.Supernotes.sheet = 'Default';
+ state.Supernotes.template = 'default';
+ state.Supernotes.title = 'name';
+ state.Supernotes.theText = '';
+ sendChat('Supernotes', '/w gm Supernotes set to Default roll template');
+ break;
+ case 'dnd5e':
+ state.Supernotes.sheet = 'D&D 5th Edition by Roll20';
+ state.Supernotes.template = 'npcaction';
+ state.Supernotes.title = 'rname';
+ state.Supernotes.theText = 'description';
+ sendChat('Supernotes', '/w gm Supernotes set to ' + state.Supernotes.sheet);
+ break;
+ case '5eshaped':
+ state.Supernotes.sheet = 'DnD 5e Shaped';
+ state.Supernotes.template = '5e-shaped';
+ state.Supernotes.title = 'title';
+ state.Supernotes.theText = 'text_big';
+ sendChat('Supernotes', '/w gm Supernotes set to ' + state.Supernotes.sheet);
+ break;
+ case 'pfcommunity':
+ state.Supernotes.sheet = 'Pathfinder Community';
+ state.Supernotes.template = 'pf_generic';
+ state.Supernotes.title = 'name';
+ state.Supernotes.theText = 'description';
+ sendChat('Supernotes', '/w gm Supernotes set to ' + state.Supernotes.sheet);
+ break;
+ case 'pfofficial':
+ state.Supernotes.sheet = 'Pathfinder by Roll20';
+ state.Supernotes.template = 'npc';
+ state.Supernotes.title = 'name';
+ state.Supernotes.theText = 'descflag=1}} {{desc';
+ sendChat('Supernotes', '/w gm Supernotes set to ' + state.Supernotes.sheet);
+ break;
+ case 'pf2e':
+ state.Supernotes.sheet = 'Pathefinder 2e';
+ state.Supernotes.template = 'rolls';
+ state.Supernotes.title = 'header';
+ state.Supernotes.theText = 'notes_show=[[1]]}} {{notes';
+ sendChat('Supernotes', '/w gm Supernotes set to ' + state.Supernotes.sheet);
+ break;
+ case 'starfinder':
+ state.Supernotes.sheet = 'Starfinder';
+ state.Supernotes.template = 'sf_generic';
+ state.Supernotes.title = 'title';
+ state.Supernotes.theText = 'buttons0';
+ sendChat('Supernotes', '/w gm Supernotes set to ' + state.Supernotes.sheet);
+ break;
+ case 'callofcthulhu':
+ state.Supernotes.sheet = 'Call of Cthulhu 7th Edition by Roll20';
+ state.Supernotes.template = 'callofcthulhu';
+ state.Supernotes.title = 'title';
+ state.Supernotes.theText = 'roll_bonus';
+ sendChat('Supernotes', '/w gm Supernotes set to ' + state.Supernotes.sheet);
+ break;
+ case 'sendtoPlayers':
+ if (state.Supernotes.sendToPlayers) {
+ state.Supernotes.sendToPlayers = false
+ } else {
+ state.Supernotes.sendToPlayers = true
+ };
+ sendChat('Supernotes', '/w gm Send to Players set to ' + state.Supernotes.sendToPlayers);
+ break;
+ case 'makeHandout':
+ if (state.Supernotes.makeHandout) {
+ state.Supernotes.makeHandout = false
+ } else {
+ state.Supernotes.makeHandout = true
+ };
+ sendChat('Supernotes', '/w gm Make Handout button set to ' + state.Supernotes.makeHandout);
+ break;
+ case 'darkMode':
+ if (state.Supernotes.darkMode) {
+ state.Supernotes.darkMode = false
+ } else {
+ state.Supernotes.darkMode = true
+ };
+ sendChat('Supernotes', '/w gm darkMode set to ' + state.Supernotes.darkMode);
+ break;
+ }
+ } else {
+ if (option !== undefined && option.includes('help')) {
+ buildSupernotesHelp();
+ return;
+ } else {
+ if (!(option + '').match(/^(card|bio|charnote|tokenimage|tooltip|avatar|imag(e|es|e[1-9]))/)) {
+ option = 'token';
+ }
+
+ let playerButton = '';
+ if (sendToPlayers && (command === '!gmnote' || command === '!selfnote')) {
+ playerButton = '\n[Send to Players](' + msg.content.replace(/!(gm|self)/, "!pc") + ')';
+ }
+
+ let handoutButton = '';
+ if (makeHandout && (command.includes('gmnote') || command.includes('selfnote'))) {
+ handoutButton = ((playerButton) ? ' | ' : '
') + '[Make Handout](' + msg.content.replace(/!(pc|self)/, "!gm") + ' --handout|NamePlaceholder|)';
+ } else {
+ //handoutButton = '\n[Make Handout](' + msg.content.replace(/!(pc|self)/, "!gm") +')';
+
+ }
+
+ let regex;
+ if (match && match[1]) {
+ regex = new RegExp(`^${match[1]}`, 'i');
+ }
+
+ let message = '';
+ let whom = '';
+
+
+
+if (option === 'card') {
+
+ (theToken || []).forEach(sel => {
+
+ const o = getObj('graphic', sel._id);
+ if (!o) return;
+
+ const tokenID = o.id;
+ const tokenName = o.get('name') || '';
+ const rawGM = o.get('gmnotes') || '';
+
+ // Always assign whom deterministically
+ whom = tokenName;
+
+ // Decode GM notes safely
+ let decodedGM = rawGM ? unescape(decodeUnicode(rawGM)) : '';
+
+ // Apply regex filtering if present
+ if (decodedGM && regex) {
+ decodedGM = _.filter(
+ decodedGM.split(/(?:[\n\r]+|
)/),
+ l => regex.test(l)
+ ).join('\r');
+ }
+
+ message = decodedGM || '';
+
+ // Crop GM-only content for player/self notes
+ if (command === '!pcnote' || command === '!selfnote') {
+ if (message.includes("-----")) {
+ message = message.split('-----')[0];
+ }
+ }
+
+ // Apply notitle
+ if (notitle) {
+ whom = '';
+ }
+
+ // Inject token image if message isn't an image URL
+ if (!/\.(png|jpg|jpeg|gif)/i.test(message)) {
+
+ let styledTokenImage = `
`;
+
+ if (!message) {
+ message = `
`;
+ }
+
+ message = styledTokenImage + message;
+ }
+
+ sendMessage(
+ whom,
+ messagePrefix,
+ template,
+ title,
+ theText,
+ message,
+ tokenID,
+ playerButton,
+ handoutButton
+ );
+
+ });
+
+ } else {
+ if (option === 'tooltip') {
+ (theToken || [])
+ .map(o => getObj('graphic', o._id))
+ .filter(g => undefined !== g)
+ .map(t => getObj('character', t.get('represents')))
+ .filter(c => undefined !== c)
+ .forEach(c => {
+ message = tokenTooltip;
+ whom = tokenName;
+ if (notitle) {
+ whom = '';
+ }
+ sendMessage(whom, messagePrefix, template, title, theText, message, tokenID, playerButton, handoutButton);
+ });
+ } else {
+ if (option === 'tokenimage') {
+ (theToken || [])
+ .map(o => getObj('graphic', o._id))
+ .filter(g => undefined !== g)
+ /* .map(t => getObj('character', t.get('represents')))*/
+ .filter(c => undefined !== c)
+ .forEach(c => {
+ message = "
";
+ whom = tokenName;
+ if (notitle) {
+ whom = '';
+ }
+ sendMessage(whom, messagePrefix, template, title, theText, message, tokenID, playerButton, handoutButton);
+ });
+ } else {
+ if (option === 'avatar') {
+ (theToken || [])
+ .map(o => getObj('graphic', o._id))
+ .filter(g => undefined !== g)
+ .map(t => getObj('character', t.get('represents')))
+ .filter(c => undefined !== c)
+ .forEach(c => {
+ message = "
";
+ whom = c.get('name');
+ if (notitle) {
+ whom = '';
+ }
+ sendMessage(whom, messagePrefix, template, title, theText, message, tokenID, playerButton, handoutButton);
+ });
+ } else {
+
+ if (option.match(/^imag(e|es|e[1-9])/)) {
+
+
+ (theToken || [])
+ .map(o => getObj('graphic', o._id))
+ .filter(g => undefined !== g)
+ .map(t => getObj('character', t.get('represents')))
+ .filter(c => undefined !== c)
+ .forEach(c => c.get('bio', (val) => {
+ if (null !== val && 'null' !== val && val.length > 0) {
+ if (regex) {
+ message = _.filter(
+ decodeUnicode(val).split(/(?:[\n\r]+|
)/),
+ (l) => regex.test(l.replace(/<[^>]*>/g, ''))
+ ).join('\r');
+ message = message.replace("
/g);
+ if (artwork === null) {
+ artwork = 'No artwork exists for this character. Consider specifiying avatar.'
+ };
+
+ } else {
+ artwork = message.match(/\<.* src.*?\>/g);
+ artwork = String(artwork);
+ if (artwork === null) {
+ artwork = 'No artwork exists for this character. Consider specifiying avatar.'
+ };
+
+
+ imageIndex = option.match(/\d+/g);
+
+
+ if (isNaN(imageIndex) || !imageIndex) {
+ imageIndex = 1
+ }
+
+ if (imageIndex > (artwork.split(",")).length) {
+ imageIndex = 1
+ }
+
+ imageIndex = imageIndex - 1; //corrects from human readable
+
+ artwork = artwork.split(",")[imageIndex];
+
+ }
+ if (('' + artwork).length > 3) {
+ message = artwork;
+ } else {
+ message = 'No artwork exists for this character.';
+ }
+ if (artwork === "null" || message === "null") {
+ message = 'No artwork exists for this character. Consider specifiying avatar.'
+ };
+
+ whom = c.get('name');
+
+ //Sends the final message
+ if (notitle) {
+ whom = '';
+ }
+ sendMessage(whom, messagePrefix, template, title, theText, message, tokenID, playerButton, handoutButton);
+
+ }
+ }));
+ } else {
+
+
+
+ if ((option === 'bio') || (option === 'charnote')) {
+ let suboption = (option === 'charnote') ? 'gmnotes' : 'bio';
+
+ (theToken || [])
+ .map(o => getObj('graphic', o._id))
+ .filter(g => undefined !== g)
+ .map(t => getObj('character', t.get('represents')))
+ .filter(c => undefined !== c)
+ .forEach(c => c.get(suboption, (val) => {
+ if (null !== val && 'null' !== val && val.length > 0) {
+ if (regex) {
+ message = _.filter(
+ decodeUnicode(val).split(/(?:[\n\r]+|
)/),
+ (l) => regex.test(l.replace(/<[^>]*>/g, ''))
+ ).join('\r');
+ } else {
+ message = decodeUnicode(val);
+ }
+ whom = c.get('name');
+ //Crops out GM info on player messages
+ if (command === '!pcnote' || command === '!selfnote') {
+ message = (message.includes("-----") ? message.split('-----')[0] : message);
+ }
+ //Sends the final message
+ if (notitle) {
+ whom = '';
+ }
+ sendMessage(whom, messagePrefix, template, title, theText, message, tokenID, playerButton, handoutButton);
+
+ } else {
+ if (notitle) {
+ whom = ''
+ }
+ message = `The information does not exist for the ${option} option`;
+ sendMessage(whom, messagePrefix, template, title, theText, message, tokenID, playerButton, handoutButton);
+
+ }
+ }));
+ } else {
+ (theToken || [])
+ .map(o => getObj('graphic', o._id))
+ .filter(g => undefined !== g)
+ .filter((o) => {
+ const gm = (o && o.get) ? o.get('gmnotes') : '';
+ return !!(gm && gm.length > 0);
+})
+ .forEach(o => {
+ if (regex) {
+ message = _.filter(unescape(decodeUnicode(o.get('gmnotes'))).split(/(?:[\n\r]+|
)/), (l) => regex.test(l)).join('\r');
+ } else {
+ message = unescape(decodeUnicode(o.get('gmnotes')));
+ }
+ whom = o.get('name');
+
+ });
+
+ //Crops out GM info on player messages
+ if (command === '!pcnote' || command === '!selfnote') {
+ message = (message.includes("-----") ? message.split('-----')[0] : message);
+ }
+
+ //Sends the final message
+ if (notitle) {
+ whom = '';
+ }
+ sendMessage(whom, messagePrefix, template, title, theText, message, tokenID, playerButton, handoutButton);
+
+ }
+
+ /* Log Block. Turn on for debugging
+ [
+ `### REPORT###`,
+ `THE MESSAGE =${message}`,
+ `command = ${command}`,
+ // `option = ${option}`,
+ `secondOption = ${secondOption}`,
+ `messagePrefix = ${messagePrefix}`,
+ `whom = ${whom}`,
+ `message =${message}`
+ ].forEach(m => log(m));
+ */
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+});
+
+{ try { throw new Error(''); } catch (e) { API_Meta.Supernotes.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.Supernotes.offset); } }
From 6fbb0c1040f3802e18f3cf447f4e1fff2b2e7431 Mon Sep 17 00:00:00 2001
From: keithcurtis1
Date: Mon, 16 Feb 2026 13:48:10 -0800
Subject: [PATCH 07/10] Update version from 0.2.5 to 0.2.6
---
Supernotes/script.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Supernotes/script.json b/Supernotes/script.json
index e75c2dd8bf..4ec70f8e39 100644
--- a/Supernotes/script.json
+++ b/Supernotes/script.json
@@ -1,7 +1,7 @@
{
"name": "Supernotes",
"script": "Supernotes.js",
- "version": "0.2.5",
+ "version": "0.2.6",
"description": "# Supernotes\r*by keithcurtis, expanded from code written by the Aaron.*\r\rThis script pulls the contents from a token's GM Notes field and sends them to chat, based on a user-selectable roll template. If the token represents a character, you can optionally pull in the Bio or GM notes from the character. The user can decide whether to whisper the notes to the GM or broadcast them to all players. Finally, there is the option to add a footer to notes whispered to the GM. This footer creates a chat button to give the option of sending the notes on to the players.\r\rThis script as written is optimized for the D&D 5th Edition by Roll20 sheet, but can be adapted easily suing the Configuration section below.\r\r* [SuperNotes forum thread](https://app.roll20.net/forum/post/8293909/script-supernotes)\r\r\r## Commands:\r\r**!gmnote** whispers the note to the GM\r\r**!pcnote** sends the note to all players\r\r**!selfnote** whispers the note to to the sender\r\r\r## Paramaters\r\r*--token* Pulls notes from the selected token's gm notes field. This is optional. If it is missing, the script assumes --token\r\r*--charnote* Pulls notes from the gm notes field of the character assigned to a token.\r\r*--bio* Pulls notes from the bio field of the character assigned to a token.\r\r*--avatar* Pulls the image from the avatar field of the character assigned to a token.\r\r--image Pulls first image from the bio field of the character assigned to a token, if any exists. Otherwise returns notice that no artwork is available\r\r*--images* Pulls all images from the bio field of the character assigned to a token, if any exist. Otherwise returns notice that no artwork is available\r\r*--image[number]* Pulls indexed image from the bio field of the character assigned to a token, if any exist. *--image1* will pull the first image, *--image2* the second and so on. Otherwise returns first image if available. If no images are available, returns notice that no artwork is available.\r\r*--notitle* This option suppresses the title in the chat output. It is useful for times when the GM might wish to show an image or note to the player without clueing them in wha the note is about. For instance, they may wish to reveal an image of a monster without revealing its name.\r\r*--id* supply this with a token id, and the script will attempt to read the notes associated with a specific token, or the character associate with that token. There is no space between --id and the token id. Only one token id may be passed.\r\r*--handout|Handoutname|* If this is present in the arguments, the note will be sent to a handout instead of chat. This can allow a note to remain usable without scrolling through the chat. It can also be used as a sort of floating palette. Notes in handouts can be updated. Running the macro again will regenerate the note. The string in between pipes will be used as the name of the note handout. If no handout by that name exists, Supernotes will create one and post a link in chat to open it. The title must be placed between two pipes. handout|My Handout| will work. handout|My Handout will break.\rA note handout automatically creates a horizontal rule at the top of the handout. Anything typed manually above that rule will be persistent. Supernotes will not overwrite this portion. You can use this area to create Journal Command Buttons to generate new notes or to give some context to the existing note. All updates are live.\r\r--template[templatename] Instead of using the configured sheet roll template, you can choose from between more than 10 custom templates that cover most common genres. Add the template command directly after the main prompt, followed by any of the regular parameters above. The current choices are:\r**template|generic.** Just the facts, ma'am. Nothing fancy here.\r**template|dark.** As above, but in reverse.\r**template|crt.** Retro greenscreen for hacking and cyberpunk. Or for reports on that xenomorph hiding on your ship.\r**template|notebook.** You know, for kids. Who like to ride bikes. Maybe they attend a school and fight vampires or rescue lost extraterrestrials\r**template|gothic.** Classic noire horror for contending with Universal monsters or maybe contending with elder gods.\r**template|apoc.** Messages scrawled on a wall. Crumbling and ancient, like the world that was.\r**template|scroll.** High fantasy. Or low fantasy—we don't judge.\r**template|lcars.** For opening hailing frequencies and to boldly split infinitives that no one has split before!\r**template|faraway.** No animated title crawl, but still has that space wizard feel.\r**template|steam.** Gears and brass have changed my life.\r**template|western.** Return with us now to those thrilling days of yesteryear!\r**template|wizard.** Like those ones that live on the coast\r**template|dragon.** Third Edition goodness!\r\r*--help* Displays help.\r\r*--config* Returns a configuration dialog box that allows you to set which sheet's roll template to use, and to toggle the '\r Players' footer.\r\r\r## Configuration\r\rWhen first installed, Supernotes is configured for the default roll template. It will display a config dialog box at startup that will allow you to choose a roll template based on your character sheet of choice, as well as the option to toggle whether you want the '\r Players' footer button to appear.\r\rYou will need to edit the code of the script if you wish to create a custom configuration, or contact keithcurtis on the Roll20 forum and request an addition. The pre-installed sheets are:\r\rDefault Template, D&D 5th Edition by Roll20, 5e Shaped, Pathfinder by Roll20, Pathfinder Community, Pathfinder 2e by Roll20, Starfinder, Starfinder, Call of Cthulhu 7th Edition by Roll20",
"authors": "Keith Curtis",
"roll20userid": "162065",
@@ -12,5 +12,5 @@
"character.represents": "read"
},
"conflicts": [],
- "previousversions": ["0.0.4","0.0.5","0.0.6","0.0.7","0.0.8","0.0.9","0.0.91","0.1.0","0.1.1","0.1.2","0.1.3","0.1.4","0.2.0","0.2.1","0.2.2","0.2.3","0.2.4","0.2.5"]
+ "previousversions": ["0.0.4","0.0.5","0.0.6","0.0.7","0.0.8","0.0.9","0.0.91","0.1.0","0.1.1","0.1.2","0.1.3","0.1.4","0.2.0","0.2.1","0.2.2","0.2.3","0.2.4","0.2.5","0.2.6"]
}
From 6cbd5710e387cecaf517b1f441685f4f6545e004 Mon Sep 17 00:00:00 2001
From: keithcurtis1
Date: Tue, 17 Feb 2026 00:00:40 -0800
Subject: [PATCH 08/10] Revise template details and add new images
Updated template descriptions and images in Supernotes.
---
Supernotes/Supernotes.js | 63 ++++++++++++++++------------------------
1 file changed, 25 insertions(+), 38 deletions(-)
diff --git a/Supernotes/Supernotes.js b/Supernotes/Supernotes.js
index a8b8bd6174..3b8bcec524 100644
--- a/Supernotes/Supernotes.js
+++ b/Supernotes/Supernotes.js
@@ -541,73 +541,60 @@ Handouts use Roll20’s native styling for cross-platform reliability.
-
-
-Script Synergy
-
-
-Templates are exposed under the object Supernotes_Templates for use in other scripts such as ScriptCards and PinNote .
-Supernotes also integrates amny of it functions with Reporter .
-
-
-For the convenience of coders who wish to integrate their scripts to use Supernotes, the CSS for the tempaltes is exposed to the global space as Supernotes_Templates.templatename. These can be found at the top of the script. Contact keithcurtis if anything is unclear.
-
-
-
Configuration
From 6d55173e01d7d65328a102e08e54bc1e7c5b7e0b Mon Sep 17 00:00:00 2001
From: keithcurtis1
Date: Tue, 17 Feb 2026 00:01:09 -0800
Subject: [PATCH 09/10] Revise template styles and descriptions in
Supernotes.js
Updated available templates section with new styles and descriptions.
---
Supernotes/0.2.6/Supernotes.js | 63 ++++++++++++++--------------------
1 file changed, 25 insertions(+), 38 deletions(-)
diff --git a/Supernotes/0.2.6/Supernotes.js b/Supernotes/0.2.6/Supernotes.js
index a8b8bd6174..3b8bcec524 100644
--- a/Supernotes/0.2.6/Supernotes.js
+++ b/Supernotes/0.2.6/Supernotes.js
@@ -541,73 +541,60 @@ Handouts use Roll20’s native styling for cross-platform reliability.
-
-
-Script Synergy
-
-
-Templates are exposed under the object Supernotes_Templates for use in other scripts such as ScriptCards and PinNote .
-Supernotes also integrates amny of it functions with Reporter .
-
-
-For the convenience of coders who wish to integrate their scripts to use Supernotes, the CSS for the tempaltes is exposed to the global space as Supernotes_Templates.templatename. These can be found at the top of the script. Contact keithcurtis if anything is unclear.
-
-
-
Configuration
From 6411b0e9880717ca7dd49e2c1d09d26d15a4233c Mon Sep 17 00:00:00 2001
From: keithcurtis1
Date: Tue, 17 Feb 2026 00:02:02 -0800
Subject: [PATCH 10/10] Add files via upload
---
Supernotes/templateimages/apoc.png | Bin 0 -> 224623 bytes
Supernotes/templateimages/choices.png | Bin 0 -> 58317 bytes
Supernotes/templateimages/crt.png | Bin 0 -> 62575 bytes
Supernotes/templateimages/dark.png | Bin 0 -> 22700 bytes
Supernotes/templateimages/dragon.png | Bin 0 -> 133458 bytes
Supernotes/templateimages/faraway.png | Bin 0 -> 39257 bytes
Supernotes/templateimages/gate3.png | Bin 0 -> 99868 bytes
Supernotes/templateimages/generic.png | Bin 0 -> 23956 bytes
Supernotes/templateimages/gothic.png | Bin 0 -> 102197 bytes
Supernotes/templateimages/lcars.png | Bin 0 -> 81601 bytes
Supernotes/templateimages/news.png | Bin 0 -> 180786 bytes
Supernotes/templateimages/notebook.png | Bin 0 -> 48423 bytes
Supernotes/templateimages/osrblue.png | Bin 0 -> 58323 bytes
Supernotes/templateimages/path.png | Bin 0 -> 43122 bytes
Supernotes/templateimages/rol200dark.png | Bin 0 -> 60373 bytes
Supernotes/templateimages/roll20light.png | Bin 0 -> 30940 bytes
Supernotes/templateimages/roman.png | Bin 0 -> 228327 bytes
Supernotes/templateimages/scroll.png | Bin 0 -> 71658 bytes
Supernotes/templateimages/scroll2.png | Bin 0 -> 147973 bytes
Supernotes/templateimages/steeam.png | Bin 0 -> 83065 bytes
Supernotes/templateimages/strange.png | Bin 0 -> 45900 bytes
Supernotes/templateimages/treasure.png | Bin 0 -> 83062 bytes
Supernotes/templateimages/vault.png | Bin 0 -> 131812 bytes
Supernotes/templateimages/western.png | Bin 0 -> 185402 bytes
Supernotes/templateimages/wizard.png | Bin 0 -> 78337 bytes
25 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 Supernotes/templateimages/apoc.png
create mode 100644 Supernotes/templateimages/choices.png
create mode 100644 Supernotes/templateimages/crt.png
create mode 100644 Supernotes/templateimages/dark.png
create mode 100644 Supernotes/templateimages/dragon.png
create mode 100644 Supernotes/templateimages/faraway.png
create mode 100644 Supernotes/templateimages/gate3.png
create mode 100644 Supernotes/templateimages/generic.png
create mode 100644 Supernotes/templateimages/gothic.png
create mode 100644 Supernotes/templateimages/lcars.png
create mode 100644 Supernotes/templateimages/news.png
create mode 100644 Supernotes/templateimages/notebook.png
create mode 100644 Supernotes/templateimages/osrblue.png
create mode 100644 Supernotes/templateimages/path.png
create mode 100644 Supernotes/templateimages/rol200dark.png
create mode 100644 Supernotes/templateimages/roll20light.png
create mode 100644 Supernotes/templateimages/roman.png
create mode 100644 Supernotes/templateimages/scroll.png
create mode 100644 Supernotes/templateimages/scroll2.png
create mode 100644 Supernotes/templateimages/steeam.png
create mode 100644 Supernotes/templateimages/strange.png
create mode 100644 Supernotes/templateimages/treasure.png
create mode 100644 Supernotes/templateimages/vault.png
create mode 100644 Supernotes/templateimages/western.png
create mode 100644 Supernotes/templateimages/wizard.png
diff --git a/Supernotes/templateimages/apoc.png b/Supernotes/templateimages/apoc.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b3d73132b9bbab1d54d739fd0320403dd9c7817
GIT binary patch
literal 224623
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 |