Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions AutoLinker/1.0.1/AutoLinker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// Script: AutoLinker
// By: Keith Curtis and Mik Holmes
// Contact: https://app.roll20.net/users/162065/keithcurtis
var API_Meta = API_Meta||{};
API_Meta.AutoLinker={offset:Number.MAX_SAFE_INTEGER,lineCount:-1};
{try{throw new Error('');}catch(e){API_Meta.AutoLinker.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-6);}}

on("ready", () => {
'use strict';

const version = '1.0.1';
log('-=> AutoLinker v' + version + ' is loaded. Type "!autolinker --help" for examples.');
//Changelog
//1.0.0 Debut
//1.0.1 Added support for Token Reference shorthand: [npc:name] becomes !tokenref <name>

let eventLockout = false;

const autolink = (str, obj) => {
const regex = /\[(?:([^\]|]*)|([^|]*)\|([^\]|]*))\]/g;
if (!str) str = "";

return str.replace(regex, (all, oneWord, link, text) => {

// =====================================================
// HEADER LINK WITHOUT PIPE
// [Handout#Header]
// =====================================================
if (oneWord && oneWord.includes("#")) {

if (!obj || obj.get("_type") !== "handout") return all;

const parts = oneWord.split("#");
const handoutName = parts[0].trim();
const headerText = parts[1] ? parts[1].trim() : "";
if (!headerText) return all;

let targetID = null;

if (handoutName === "") {
targetID = obj.get("id");
} else {
const found = findObjs(
{ _type: "handout", name: handoutName },
{ caseInsensitive: true }
);
if (found && found[0]) targetID = found[0].get("id");
else return all;
}

const cleanHeader = headerText.replace(/<[^>]*>/g, "");
const encodedHeader = cleanHeader.replace(/ /g, "%20");
const url = `http://journal.roll20.net/handout/${targetID}/#${encodedHeader}`;

// Display text defaults to header text
return `<a href='${url}'>${cleanHeader}</a>`;
}




// =====================================================
// SINGLE WORD MODE (namespace links)
// =====================================================
if (oneWord && oneWord.includes(":")) {
const spell = oneWord.split(":");
switch (spell[0]) {
case "5e":
return `<i><a href='https://roll20.net/compendium/dnd5e/${spell[1]}'>${spell[1]}</a></i>`;
case "pf2":
return `<i><a href='https://roll20.net/compendium/pf2/${spell[1]}'>${spell[1]}</a></i>`;
case "npc":
return `<a href="!tokenref ${spell.slice(1).join(":")}">${spell.slice(1).join(":")}</a>`;
case "gr":
return `<a href="\`/gmroll ${spell[1]}">${spell[1]}</a>`;
case "r":
return `<a href="\`/roll ${spell[1]}">${spell[1]}</a>`;
case "sot-quote":
return `<div style="${styles.sot.quote}">${spell[1]}</div>`;
default:
return all;
}
}

// =====================================================
// PIPE MODE
// =====================================================
if (link && text) {

// HEADER LINK WITH PIPE
// [Handout#Header|Text]
if (obj && obj.get("_type") === "handout" && link.includes("#")) {

const parts = link.split("#");
const handoutName = parts[0].trim();
const headerText = parts[1] ? parts[1].trim() : "";
if (!headerText) return all;

let targetID = null;

if (handoutName === "") {
targetID = obj.get("id");
} else {
const found = findObjs(
{ _type: "handout", name: handoutName },
{ caseInsensitive: true }
);
if (found && found[0]) targetID = found[0].get("id");
else return all;
}

const cleanHeader = headerText.replace(/<[^>]*>/g, "");
const encodedHeader = cleanHeader.replace(/ /g, "%20");
const url = `http://journal.roll20.net/handout/${targetID}/#${encodedHeader}`;

return `<a href='${url}'>${text}</a>`;
}

// NAMESPACE LINKS WITH PIPE
if (link.includes(":")) {
const spell = link.split(":");
switch (spell[0]) {
case "5e":
return `<i><a href='https://roll20.net/compendium/dnd5e/${spell[1]}'>${text}</a></i>`;
case "pf2":
return `<i><a href='https://roll20.net/compendium/pf2/${spell[1]}'>${text}</a></i>`;
case "npc":
return `<a href="!tokenref ${spell.slice(1).join(":")}">${text}</a>`;
default:
return all;
}
}

// JOURNAL LINKS
const targetObj = findObjs({ name: link }, { caseInsensitive: true });
if (targetObj[0]) {
const targetID = targetObj[0].get("id");
const targetType = targetObj[0].get("type");

if (targetType === "handout")
return `<a href='http://journal.roll20.net/handout/${targetID}'>${text}</a>`;
else if (targetType === "character")
return `<a href='http://journal.roll20.net/character/${targetID}'>${text}</a>`;
}
}

return all;
});
};

const runAutolink = (obj, field) => {
if (!eventLockout) {
eventLockout = true;

obj.get(field, str => {
const newText = autolink(str, obj);
if (newText !== str) obj.set(field, newText);
eventLockout = false;
});
}
};


/* ============================================================
* AUTOLINKER HELP
* Triggered by: !autolinker --help
* ============================================================ */

const showAutoLinkerHelp = function(playerid) {

let helpText =
"<p><span style='font-weight:bold; font-size:24px;'>Autolinker Help</span></p>" +
"<p>Some examples of the autolinker functionality. These can be used on the notes/gmnotes of any handout or character.</p>" +
"<p>Please note that this script works after you save changes to a handout, " +
"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>" +
"<p><code>[goblin|Jimmy]</code> will make a link with the text 'Jimmy' to the 'goblin' handout.</p>" +
"<p><code>[5e:fireball]</code> will link to the 5e compendium page for fireball.</p>" +
"<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>" +
"<p>Currently <code>5e:</code> and <code>pf2:</code> will link to their respective compendiums.</p>" +
"<p><b>Handout Header linking:</b></p>" +
"<p>To link to specific headers in a handout (handouts only) use the # character.</p>" +
"<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>" +
"<p>If the link goes to a header in the same handout, you do not need to specify the handout:</p>" +
"<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>" +
"<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>" +
"<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>";

let styledDiv =
"<div style='background-color:#bbb; padding:12px; border-radius:10px; border:2px solid #888; color:#111'>" +
helpText +
"</div>";

let player = getObj("player", playerid);
if (player) {
sendChat("AutoLinker", "/w \"" + player.get("_displayname") + "\" " + styledDiv);
}
};


/* ============================================================
* CHAT HANDLER
* ============================================================ */

on("chat:message", function(msg) {
if (msg.type !== "api") return;

if (msg.content.trim() === "!autolinker --help") {
showAutoLinkerHelp(msg.playerid);
}
});



const registerEventHandlers = () => {
on('change:handout:notes', obj => runAutolink(obj, "notes"));
on('change:handout:gmnotes', obj => runAutolink(obj, "gmnotes"));
on('change:character:bio', obj => runAutolink(obj, "bio"));
on('change:character:gmnotes', obj => runAutolink(obj, "gmnotes"));
};

registerEventHandlers();
});

{try{throw new Error('');}catch(e){API_Meta.AutoLinker.lineCount=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-API_Meta.AutoLinker.offset);}}
12 changes: 10 additions & 2 deletions AutoLinker/AutoLinker.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ API_Meta.AutoLinker={offset:Number.MAX_SAFE_INTEGER,lineCount:-1};
on("ready", () => {
'use strict';

const version = '1.0.0';
const version = '1.0.1';
log('-=> AutoLinker v' + version + ' is loaded. Type "!autolinker --help" for examples.');
//Changelog
//1.0.0 Debut
//1.0.1 Added support for Token Reference shorthand: [npc:name] becomes !tokenref <name>

let eventLockout = false;

Expand Down Expand Up @@ -55,6 +56,9 @@ const autolink = (str, obj) => {
return `<a href='${url}'>${cleanHeader}</a>`;
}




// =====================================================
// SINGLE WORD MODE (namespace links)
// =====================================================
Expand All @@ -65,6 +69,8 @@ const autolink = (str, obj) => {
return `<i><a href='https://roll20.net/compendium/dnd5e/${spell[1]}'>${spell[1]}</a></i>`;
case "pf2":
return `<i><a href='https://roll20.net/compendium/pf2/${spell[1]}'>${spell[1]}</a></i>`;
case "npc":
return `<a href="!tokenref ${spell.slice(1).join(":")}">${spell.slice(1).join(":")}</a>`;
case "gr":
return `<a href="\`/gmroll ${spell[1]}">${spell[1]}</a>`;
case "r":
Expand Down Expand Up @@ -118,7 +124,9 @@ const autolink = (str, obj) => {
return `<i><a href='https://roll20.net/compendium/dnd5e/${spell[1]}'>${text}</a></i>`;
case "pf2":
return `<i><a href='https://roll20.net/compendium/pf2/${spell[1]}'>${text}</a></i>`;
default:
case "npc":
return `<a href="!tokenref ${spell.slice(1).join(":")}">${text}</a>`;
default:
return all;
}
}
Expand Down
4 changes: 2 additions & 2 deletions AutoLinker/script.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "AutoLinker",
"script": "AutoLinker.js",
"version": "1.0.0",
"version": "1.0.1",
"description": "# Autolinker\n\n## Purpose\n\nAutolinker converts bracketed shorthand written in the Notes or GMNotes fields of handouts and characters into clickable Roll20 journal or compendium links when the entry is saved. This extends the basic linking functions built into Roll20.\n\n## General Usage\n\nThese formats may be used in the Notes or GMNotes fields of any handout or character.\n\nNote: The script runs after a save event. Because the handout may refresh before processing finishes, you may need to close and reopen the handout (or click Edit again) to see the updated links.\n\n## Journal Links\n\n[goblin|Jimmy]\n\nCreates a link to the handout or character named goblin, displayed as Jimmy.\n\nIf no display text is provided, standard Roll20 journal linking rules apply.\n\n## Compendium Links\n\n[5e:fireball]\n\nLinks to the D&D 5e compendium entry for fireball.\n\n[5e:wall of fire|the wall]\n\nLinks to the D&D 5e compendium entry for wall of fire, displayed as the wall.\n\n### Supported Compendium Prefixes\n\n- 5e: — D&D 5th Edition\n- pf2: — Pathfinder 2nd Edition\n\n## Handout Header Linking\n\nHeader links apply to handouts only and use the # character.\n\n### Link to a Header in Another Handout\n\n[Dungeon of Doom#6. Zombie Chorus|See Room 6]\n\nLinks to the header 6. Zombie Chorus in the handout Dungeon of Doom, displayed as See Room 6.\n\n### Link to a Header in the Same Handout\n\n[#6. Zombie Chorus|See Room 6]\n\nLinks to the header 6. Zombie Chorus in the current handout, displayed as See Room 6.\n\n### Omit Display Text\n\n[#6. Zombie Chorus]\n\nIf no display text is supplied, the header text is used as the link text.",
"authors": "Keith Curtis",
"roll20userid": "162065",
Expand All @@ -12,6 +12,6 @@
},
"conflicts": [],
"previousversions": [
"1.0.0"
"1.0.0", "1.0.1"
]
}
Loading
Loading