Skip to content

Commit f7aef9f

Browse files
authored
Merge pull request #2179 from keithcurtis1/master
PinNote
2 parents 24e44a2 + 6411b0e commit f7aef9f

32 files changed

Lines changed: 2711 additions & 22 deletions

PInNote/1.0.0/PinNote.js

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
// Script: PinNote
2+
// By: Keith Curtis
3+
// Contact: https://app.roll20.net/users/162065/keithcurtis
4+
var API_Meta = API_Meta||{}; //eslint-disable-line no-var
5+
API_Meta.PinNote={offset:Number.MAX_SAFE_INTEGER,lineCount:-1};
6+
{try{throw new Error('');}catch(e){API_Meta.PinNote.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-6);}}
7+
8+
9+
(() => {
10+
'use strict';
11+
12+
const version = '1.0.0'; //version number set here
13+
log('-=> PinNote v' + version + ' is loaded.');
14+
//Changelog
15+
//1.0.0 Debut
16+
17+
18+
const SCRIPT_NAME = 'PinNote';
19+
20+
const isGMPlayer = (playerid) => playerIsGM(playerid);
21+
22+
const getTemplate = (name) => {
23+
if (typeof Supernotes_Templates === 'undefined') {
24+
return null;
25+
}
26+
if (!name) return Supernotes_Templates.generic;
27+
const key = name.toLowerCase();
28+
return Supernotes_Templates[key] || Supernotes_Templates.generic;
29+
};
30+
31+
const sendGenericError = (msg, text) => {
32+
if (typeof Supernotes_Templates === 'undefined') return;
33+
34+
const t = Supernotes_Templates.generic;
35+
sendChat(
36+
SCRIPT_NAME,
37+
t.boxcode +
38+
t.titlecode + SCRIPT_NAME +
39+
t.textcode + text +
40+
'</div></div>' +
41+
t.footer +
42+
'</div>'
43+
);
44+
};
45+
46+
/* ============================================================
47+
* HEADER COLOR ENFORCEMENT
48+
* ============================================================ */
49+
50+
const enforceHeaderColor = (html, template) => {
51+
if (!html) return html;
52+
53+
const colorMatch = template.textcode.match(/color\s*:\s*([^;"]+)/i);
54+
if (!colorMatch) return html;
55+
56+
const colorValue = colorMatch[1].trim();
57+
58+
return html.replace(
59+
/<(h[1-4])\b([^>]*)>/gi,
60+
(match, tag, attrs) => {
61+
62+
if (/style\s*=/i.test(attrs)) {
63+
return `<${tag}${attrs.replace(
64+
/style\s*=\s*["']([^"']*)["']/i,
65+
(m, styleContent) =>
66+
`style="${styleContent}; color: ${colorValue};"`
67+
)}>`;
68+
}
69+
70+
return `<${tag}${attrs} style="color: ${colorValue};">`;
71+
}
72+
);
73+
};
74+
75+
/* ============================================================ */
76+
77+
const parseArgs = (content) => {
78+
const args = {};
79+
content.replace(/--([^|]+)\|([^\s]+)/gi, (_, k, v) => {
80+
args[k.toLowerCase()] = v.toLowerCase();
81+
return '';
82+
});
83+
return args;
84+
};
85+
86+
const extractHandoutSection = ({ handout, subLink, subLinkType }) => {
87+
return new Promise((resolve) => {
88+
89+
if (!handout) return resolve(null);
90+
91+
if (!subLink) {
92+
const field = subLinkType === 'headerGM' ? 'gmnotes' : 'notes';
93+
handout.get(field, (content) => resolve(content || null));
94+
return;
95+
}
96+
97+
if (!['headerplayer', 'headergm'].includes(subLinkType?.toLowerCase())) {
98+
return resolve(null);
99+
}
100+
101+
const field = subLinkType.toLowerCase() === 'headergm'
102+
? 'gmnotes'
103+
: 'notes';
104+
105+
handout.get(field, (content) => {
106+
if (!content) return resolve(null);
107+
108+
const headerRegex = /<(h[1-4])\b[^>]*>([\s\S]*?)<\/\1>/gi;
109+
let match;
110+
111+
while ((match = headerRegex.exec(content)) !== null) {
112+
const tagName = match[1];
113+
const innerHTML = match[2];
114+
const stripped = innerHTML.replace(/<[^>]+>/g, '');
115+
116+
if (stripped === subLink) {
117+
const level = parseInt(tagName[1], 10);
118+
const startIndex = match.index;
119+
120+
const remainder = content.slice(headerRegex.lastIndex);
121+
122+
const stopRegex = new RegExp(
123+
`<h([1-${level}])\\b[^>]*>`,
124+
'i'
125+
);
126+
127+
const stopMatch = stopRegex.exec(remainder);
128+
129+
const endIndex = stopMatch
130+
? headerRegex.lastIndex + stopMatch.index
131+
: content.length;
132+
133+
return resolve(content.slice(startIndex, endIndex));
134+
}
135+
}
136+
137+
resolve(null);
138+
});
139+
});
140+
};
141+
142+
const transformBlockquoteMode = (html) => {
143+
144+
const blockRegex = /<blockquote\b[^>]*>([\s\S]*?)<\/blockquote>/gi;
145+
146+
let match;
147+
let lastIndex = 0;
148+
let playerContent = '';
149+
let gmContent = '';
150+
let found = false;
151+
152+
while ((match = blockRegex.exec(html)) !== null) {
153+
found = true;
154+
gmContent += html.slice(lastIndex, match.index);
155+
playerContent += match[1];
156+
lastIndex = blockRegex.lastIndex;
157+
}
158+
159+
gmContent += html.slice(lastIndex);
160+
161+
if (!found) {
162+
return { player: '', gm: html };
163+
}
164+
165+
return { player: playerContent, gm: gmContent };
166+
};
167+
168+
on('chat:message', async (msg) => {
169+
if (msg.type !== 'api' || !msg.content.startsWith('!pinnote')) return;
170+
171+
if (typeof Supernotes_Templates === 'undefined') {
172+
sendChat(SCRIPT_NAME, `/w gm PinNote requires Supernotes_Templates to be loaded.`);
173+
return;
174+
}
175+
176+
const args = parseArgs(msg.content);
177+
const isGM = isGMPlayer(msg.playerid);
178+
179+
if (!msg.selected || msg.selected.length === 0)
180+
return sendGenericError(msg, 'No pin selected.');
181+
182+
const sel = msg.selected.find(s => s._type === 'pin');
183+
if (!sel)
184+
return sendGenericError(msg, 'Selected object is not a pin.');
185+
186+
const pin = getObj('pin', sel._id);
187+
if (!pin)
188+
return sendGenericError(msg, 'Selected pin could not be resolved.');
189+
190+
const isSynced =
191+
!pin.get('notesDesynced') &&
192+
!pin.get('gmNotesDesynced') &&
193+
!pin.get('imageDesynced');
194+
195+
const linkType = pin.get('linkType');
196+
197+
/* ============================================================
198+
* LINKED HANDOUT MODE
199+
* ============================================================ */
200+
201+
if (isSynced && linkType === 'handout') {
202+
203+
const handoutId = pin.get('link');
204+
const subLink = pin.get('subLink');
205+
const subLinkType = pin.get('subLinkType');
206+
const autoNotesType = pin.get('autoNotesType');
207+
208+
const handout = getObj('handout', handoutId);
209+
if (!handout)
210+
return sendGenericError(msg, 'Linked handout not found.');
211+
212+
let extracted = await extractHandoutSection({
213+
handout,
214+
subLink,
215+
subLinkType
216+
});
217+
218+
if (!extracted)
219+
return sendGenericError(msg, 'Requested section not found in handout.');
220+
221+
const template = getTemplate(args.template);
222+
if (!template) return;
223+
224+
const sender = pin.get('title') || SCRIPT_NAME;
225+
const titleText = subLink || sender;
226+
227+
if (subLink) {
228+
const headerStripRegex = /^<h[1-4]\b[^>]*>[\s\S]*?<\/h[1-4]>/i;
229+
extracted = extracted.replace(headerStripRegex, '');
230+
}
231+
232+
let to = (args.to || 'pc').toLowerCase();
233+
if (!isGM) to = 'pc';
234+
235+
let whisperPrefix = '';
236+
const extractingGM = subLinkType?.toLowerCase() === 'headergm';
237+
238+
let visibleContent = extracted;
239+
let gmBlock = '';
240+
241+
if (autoNotesType === 'blockquote') {
242+
243+
const transformed = transformBlockquoteMode(extracted);
244+
245+
visibleContent = enforceHeaderColor(transformed.player, template);
246+
247+
if (transformed.gm && to !== 'pc') {
248+
gmBlock =
249+
`<div style=${template.whisperStyle}>` +
250+
enforceHeaderColor(transformed.gm, template) +
251+
`</div>`;
252+
}
253+
254+
} else {
255+
visibleContent = enforceHeaderColor(visibleContent, template);
256+
}
257+
258+
if (extractingGM) {
259+
whisperPrefix = '/w gm ';
260+
} else if (to === 'gm') {
261+
whisperPrefix = '/w gm ';
262+
} else if (to === 'self') {
263+
whisperPrefix = `/w "${msg.who}" `;
264+
}
265+
266+
const html =
267+
template.boxcode +
268+
template.titlecode + titleText +
269+
template.textcode +
270+
(visibleContent || '') +
271+
gmBlock +
272+
'</div></div>' +
273+
template.footer +
274+
'</div>';
275+
276+
sendChat(sender, whisperPrefix + html);
277+
return;
278+
}
279+
280+
/* ============================================================
281+
* CUSTOM PIN MODE
282+
* ============================================================ */
283+
284+
if (
285+
!pin.get('notesDesynced') &&
286+
!pin.get('gmNotesDesynced') &&
287+
!pin.get('imageDesynced')
288+
) {
289+
return sendGenericError(
290+
msg,
291+
'This pin is not desynced from its linked handout.'
292+
);
293+
}
294+
295+
const notes = (pin.get('notes') || '').trim();
296+
if (!notes)
297+
return sendGenericError(msg, 'This pin has no notes to display.');
298+
299+
let to = (args.to || 'pc').toLowerCase();
300+
if (!isGM) to = 'pc';
301+
302+
let whisperPrefix = '';
303+
if (to === 'gm') whisperPrefix = '/w gm ';
304+
else if (to === 'self') whisperPrefix = `/w "${msg.who}" `;
305+
306+
const template = getTemplate(args.template);
307+
if (!template) return;
308+
309+
const sender = pin.get('title') || SCRIPT_NAME;
310+
311+
let imageBlock = '';
312+
const tooltipImage = pin.get('tooltipImage');
313+
if (tooltipImage) {
314+
imageBlock =
315+
`<img src="${tooltipImage}" ` +
316+
`style="max-width:100%; max-height:400px; display:block; margin-bottom:8px;">`;
317+
}
318+
319+
const coloredNotes = enforceHeaderColor(notes, template);
320+
321+
let gmBlock = '';
322+
if (isGM && to !== 'pc' && pin.get('gmNotes')) {
323+
gmBlock =
324+
`<div style=${template.whisperStyle}>` +
325+
enforceHeaderColor(pin.get('gmNotes'), template) +
326+
`</div>`;
327+
}
328+
329+
const html =
330+
template.boxcode +
331+
template.titlecode + sender +
332+
template.textcode +
333+
imageBlock +
334+
coloredNotes +
335+
gmBlock +
336+
'</div></div>' +
337+
template.footer +
338+
'</div>';
339+
340+
sendChat(sender, whisperPrefix + html);
341+
});
342+
343+
})();
344+
345+
{try{throw new Error('');}catch(e){API_Meta.PinNote.lineCount=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-API_Meta.PinNote.offset);}}

0 commit comments

Comments
 (0)