1+ // Script: AutoLinker
2+ // By: Keith Curtis and Mik Holmes
3+ // Contact: https://app.roll20.net/users/162065/keithcurtis
4+ var API_Meta = API_Meta || { } ;
5+ API_Meta . AutoLinker = { offset :Number . MAX_SAFE_INTEGER , lineCount :- 1 } ;
6+ { try { throw new Error ( '' ) ; } catch ( e ) { API_Meta . AutoLinker . offset = ( parseInt ( e . stack . split ( / \n / ) [ 1 ] . replace ( / ^ .* : ( \d + ) : .* $ / , '$1' ) , 10 ) - 6 ) ; } }
7+
8+ on ( "ready" , ( ) => {
9+ 'use strict' ;
10+
11+ const version = '1.0.0' ;
12+ log ( '-=> AutoLinker v' + version + ' is loaded. Type "!autolinker --help" for examples.' ) ;
13+ //Changelog
14+ //1.0.0 Debut
15+
16+ let eventLockout = false ;
17+
18+ const autolink = ( str , obj ) => {
19+ const regex = / \[ (?: ( [ ^ \] | ] * ) | ( [ ^ | ] * ) \| ( [ ^ \] | ] * ) ) \] / g;
20+ if ( ! str ) str = "" ;
21+
22+ return str . replace ( regex , ( all , oneWord , link , text ) => {
23+
24+ // =====================================================
25+ // HEADER LINK WITHOUT PIPE
26+ // [Handout#Header]
27+ // =====================================================
28+ if ( oneWord && oneWord . includes ( "#" ) ) {
29+
30+ if ( ! obj || obj . get ( "_type" ) !== "handout" ) return all ;
31+
32+ const parts = oneWord . split ( "#" ) ;
33+ const handoutName = parts [ 0 ] . trim ( ) ;
34+ const headerText = parts [ 1 ] ? parts [ 1 ] . trim ( ) : "" ;
35+ if ( ! headerText ) return all ;
36+
37+ let targetID = null ;
38+
39+ if ( handoutName === "" ) {
40+ targetID = obj . get ( "id" ) ;
41+ } else {
42+ const found = findObjs (
43+ { _type : "handout" , name : handoutName } ,
44+ { caseInsensitive : true }
45+ ) ;
46+ if ( found && found [ 0 ] ) targetID = found [ 0 ] . get ( "id" ) ;
47+ else return all ;
48+ }
49+
50+ const cleanHeader = headerText . replace ( / < [ ^ > ] * > / g, "" ) ;
51+ const encodedHeader = cleanHeader . replace ( / / g, "%20" ) ;
52+ const url = `http://journal.roll20.net/handout/${ targetID } /#${ encodedHeader } ` ;
53+
54+ // Display text defaults to header text
55+ return `<a href='${ url } '>${ cleanHeader } </a>` ;
56+ }
57+
58+ // =====================================================
59+ // SINGLE WORD MODE (namespace links)
60+ // =====================================================
61+ if ( oneWord && oneWord . includes ( ":" ) ) {
62+ const spell = oneWord . split ( ":" ) ;
63+ switch ( spell [ 0 ] ) {
64+ case "5e" :
65+ return `<i><a href='https://roll20.net/compendium/dnd5e/${ spell [ 1 ] } '>${ spell [ 1 ] } </a></i>` ;
66+ case "pf2" :
67+ return `<i><a href='https://roll20.net/compendium/pf2/${ spell [ 1 ] } '>${ spell [ 1 ] } </a></i>` ;
68+ case "gr" :
69+ return `<a href="\`/gmroll ${ spell [ 1 ] } ">${ spell [ 1 ] } </a>` ;
70+ case "r" :
71+ return `<a href="\`/roll ${ spell [ 1 ] } ">${ spell [ 1 ] } </a>` ;
72+ case "sot-quote" :
73+ return `<div style="${ styles . sot . quote } ">${ spell [ 1 ] } </div>` ;
74+ default :
75+ return all ;
76+ }
77+ }
78+
79+ // =====================================================
80+ // PIPE MODE
81+ // =====================================================
82+ if ( link && text ) {
83+
84+ // HEADER LINK WITH PIPE
85+ // [Handout#Header|Text]
86+ if ( obj && obj . get ( "_type" ) === "handout" && link . includes ( "#" ) ) {
87+
88+ const parts = link . split ( "#" ) ;
89+ const handoutName = parts [ 0 ] . trim ( ) ;
90+ const headerText = parts [ 1 ] ? parts [ 1 ] . trim ( ) : "" ;
91+ if ( ! headerText ) return all ;
92+
93+ let targetID = null ;
94+
95+ if ( handoutName === "" ) {
96+ targetID = obj . get ( "id" ) ;
97+ } else {
98+ const found = findObjs (
99+ { _type : "handout" , name : handoutName } ,
100+ { caseInsensitive : true }
101+ ) ;
102+ if ( found && found [ 0 ] ) targetID = found [ 0 ] . get ( "id" ) ;
103+ else return all ;
104+ }
105+
106+ const cleanHeader = headerText . replace ( / < [ ^ > ] * > / g, "" ) ;
107+ const encodedHeader = cleanHeader . replace ( / / g, "%20" ) ;
108+ const url = `http://journal.roll20.net/handout/${ targetID } /#${ encodedHeader } ` ;
109+
110+ return `<a href='${ url } '>${ text } </a>` ;
111+ }
112+
113+ // NAMESPACE LINKS WITH PIPE
114+ if ( link . includes ( ":" ) ) {
115+ const spell = link . split ( ":" ) ;
116+ switch ( spell [ 0 ] ) {
117+ case "5e" :
118+ return `<i><a href='https://roll20.net/compendium/dnd5e/${ spell [ 1 ] } '>${ text } </a></i>` ;
119+ case "pf2" :
120+ return `<i><a href='https://roll20.net/compendium/pf2/${ spell [ 1 ] } '>${ text } </a></i>` ;
121+ default :
122+ return all ;
123+ }
124+ }
125+
126+ // JOURNAL LINKS
127+ const targetObj = findObjs ( { name : link } , { caseInsensitive : true } ) ;
128+ if ( targetObj [ 0 ] ) {
129+ const targetID = targetObj [ 0 ] . get ( "id" ) ;
130+ const targetType = targetObj [ 0 ] . get ( "type" ) ;
131+
132+ if ( targetType === "handout" )
133+ return `<a href='http://journal.roll20.net/handout/${ targetID } '>${ text } </a>` ;
134+ else if ( targetType === "character" )
135+ return `<a href='http://journal.roll20.net/character/${ targetID } '>${ text } </a>` ;
136+ }
137+ }
138+
139+ return all ;
140+ } ) ;
141+ } ;
142+
143+ const runAutolink = ( obj , field ) => {
144+ if ( ! eventLockout ) {
145+ eventLockout = true ;
146+
147+ obj . get ( field , str => {
148+ const newText = autolink ( str , obj ) ;
149+ if ( newText !== str ) obj . set ( field , newText ) ;
150+ eventLockout = false ;
151+ } ) ;
152+ }
153+ } ;
154+
155+
156+ /* ============================================================
157+ * AUTOLINKER HELP
158+ * Triggered by: !autolinker --help
159+ * ============================================================ */
160+
161+ const showAutoLinkerHelp = function ( playerid ) {
162+
163+ let helpText =
164+ "<p><span style='font-weight:bold; font-size:24px;'>Autolinker Help</span></p>" +
165+ "<p>Some examples of the autolinker functionality. These can be used on the notes/gmnotes of any handout or character.</p>" +
166+ "<p>Please note that this script works after you save changes to a handout, " +
167+ "but the handout often reloads before the script is finished. Closing and reopening the handout, or clicking Edit again, should give it enough time to properly link things.</p>" +
168+ "<p><code>[goblin|Jimmy]</code> will make a link with the text 'Jimmy' to the 'goblin' handout.</p>" +
169+ "<p><code>[5e:fireball]</code> will link to the 5e compendium page for fireball.</p>" +
170+ "<p><code>[5e:wall of fire|the wall]</code> will make a link with the text 'the wall' to the 5e compendium page for wall of fire</p>" +
171+ "<p>Currently <code>5e:</code> and <code>pf2:</code> will link to their respective compendiums.</p>" +
172+ "<p><b>Handout Header linking:</b></p>" +
173+ "<p>To link to specific headers in a handout (handouts only) use the # character.</p>" +
174+ "<p><code>[Dungeon of Doom#6. Zombie Chorus|See Room 6]</code> will link the header '6. Zombie Chorus' in the handout 'Dungeon of Doom', with the display text 'See Room 6'.</p>" +
175+ "<p>If the link goes to a header in the same handout, you do not need to specify the handout:</p>" +
176+ "<p><code>[#6. Zombie Chorus|See Room 6]</code> will link the header '6. Zombie Chorus' in the same handout, with the display text 'See Room 6'.</p>" +
177+ "<p>If you do not need the display text of the link to be different from the text of the header, you can omit that part as well:</p>" +
178+ "<p><code>[#6. Zombie Chorus]</code> will link the header '6. Zombie Chorus' in the same handout, with the display text '6. Zombie Chorus'.</p>" ;
179+
180+ let styledDiv =
181+ "<div style='background-color:#bbb; padding:12px; border-radius:10px; border:2px solid #888; color:#111'>" +
182+ helpText +
183+ "</div>" ;
184+
185+ let player = getObj ( "player" , playerid ) ;
186+ if ( player ) {
187+ sendChat ( "AutoLinker" , "/w \"" + player . get ( "_displayname" ) + "\" " + styledDiv ) ;
188+ }
189+ } ;
190+
191+
192+ /* ============================================================
193+ * CHAT HANDLER
194+ * ============================================================ */
195+
196+ on ( "chat:message" , function ( msg ) {
197+ if ( msg . type !== "api" ) return ;
198+
199+ if ( msg . content . trim ( ) === "!autolinker --help" ) {
200+ showAutoLinkerHelp ( msg . playerid ) ;
201+ }
202+ } ) ;
203+
204+
205+
206+ const registerEventHandlers = ( ) => {
207+ on ( 'change:handout:notes' , obj => runAutolink ( obj , "notes" ) ) ;
208+ on ( 'change:handout:gmnotes' , obj => runAutolink ( obj , "gmnotes" ) ) ;
209+ on ( 'change:character:bio' , obj => runAutolink ( obj , "bio" ) ) ;
210+ on ( 'change:character:gmnotes' , obj => runAutolink ( obj , "gmnotes" ) ) ;
211+ } ;
212+
213+ registerEventHandlers ( ) ;
214+ } ) ;
215+
216+ { try { throw new Error ( '' ) ; } catch ( e ) { API_Meta . AutoLinker . lineCount = ( parseInt ( e . stack . split ( / \n / ) [ 1 ] . replace ( / ^ .* : ( \d + ) : .* $ / , '$1' ) , 10 ) - API_Meta . AutoLinker . offset ) ; } }
0 commit comments