Tools for Kanking.
Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greatest.deepsurf.us/scripts/508835/1449206/Kanka%20SDK%20%28dev%29.js
// ==UserScript==
// @name Kanka SDK (dev)
// @namespace https://greatest.deepsurf.us/en/users/1029479-infinitegeek
// @version 0.0.1-3
// @description Tools for Kanking.
// @author InfiniteGeek
// @supportURL Infinite @ https://discord.gg/rhsyZJ4
// @license MIT
// @match https://app.kanka.io/w/*
// @icon https://www.google.com/s2/favicons?domain=kanka.io
// @keywords kanka,sdk
// @grant none
// ==/UserScript==
/******/ (() => { // webpackBootstrap
/******/ "use strict";
var _a, _b;
const emit_debug = (...args) => { };
//const emit_debug = console.log;
function getElementPromise(...selectorChain) {
let intervalHandle;
let doc;
return new Promise((resolve, reject) => {
const getElement = () => {
if (!jQuery)
return undefined;
try {
let lmnt = (doc !== null && doc !== void 0 ? doc : (doc = jQuery(document.documentElement)));
const selectors = [...selectorChain];
let selector = null;
while (selector = selectors.shift()) {
lmnt = lmnt.find(selector);
if (!lmnt)
return undefined;
}
if (!lmnt)
return null;
intervalHandle && clearInterval(intervalHandle);
resolve(lmnt);
return lmnt;
}
catch (error) {
intervalHandle && clearInterval(intervalHandle);
reject(error);
return null;
}
};
if (typeof MutationObserver) {
// if we have the MutationObserver API, hook to document changes
const observer = new MutationObserver(() => getElement() && observer.disconnect());
observer.observe(document.documentElement, { childList: true, subtree: true });
}
else {
// if not, use a sad timer
intervalHandle = setInterval(getElement, 333);
}
});
}
const Api = {
getXMLHttpRequest: (method) => {
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open(method, Uri.buildUri(Entity.entityType, Entity.typedID), false);
Api.headers.setCsrf(xhr);
Api.headers.setXMLHttpRequest(xhr);
return xhr;
},
headers: {
setCsrf: (xhr) => xhr.setRequestHeader('x-csrf-token', Session.csrfToken),
setXMLHttpRequest: (xhr) => xhr.setRequestHeader('x-requested-with', 'XMLHttpRequest'),
},
createPostParams: () => {
const params = new URLSearchParams();
params.append('_token', Session.csrfToken);
params.append('datagrid-action', 'batch');
// this needs the plural
params.append('entity', Entity.entityType);
params.append('mode', 'table');
// typedID is different from entityID
params.append('models', Entity.typedID);
params.append('undefined', '');
return params;
},
fetch_success: async (response) => {
var _a;
emit_debug('Success:', response);
window.showToast(response.statusText, 'bg-success text-success-content');
return { ok: response.ok, document: (_a = $.parseHTML(await response.text())) !== null && _a !== void 0 ? _a : [] };
},
post: (url, body) => {
return fetch(url, {
method: 'POST',
redirect: 'follow',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
})
.then(Api.fetch_success)
.catch((error) => {
console.error('Error:', error);
window.showToast(error, 'bg-primary text-error-content');
return { ok: false, document: [], error };
});
}
};
/**
* Extract metadata from the classes on the <body>
*/
function parseBodyClasses(body) {
const classes = Array.from(body.classList);
const entity = { id: '', entityType: 'default', type: '' };
const tags = [];
const kankaClassRegex = /^kanka-(\w+)-(\w+)$/;
let tempTag = null;
function processTag(isValueNumeric, value) {
// tags are emitted as id/name pairs
// parent tags also end up in the list as ID-only entries
// any name is associated with the ID prior
if (isValueNumeric) {
tempTag = value;
}
else if (tempTag !== null) {
tags.push({ id: tempTag, entityType: value });
tempTag = null;
}
}
classes
.map(className => className.match(kankaClassRegex))
.filter(match => !!match)
.forEach((match) => {
const [, key, value] = match;
const isValueNumeric = !isNaN(Number(value));
switch (key) {
// kanka-entity-{entityID} kanka-entity-{entityType}
case 'entity':
if (isValueNumeric) {
entity['id'] = value;
}
else {
entity['entityType'] = value;
}
break;
// kanka-type-{typeValue}
case 'type':
entity.type = value;
break;
// kanka-tag-{id} kanka-tag-{name}
case 'tag':
processTag(isValueNumeric, value);
break;
default:
console.warn("What's this? 💀🎃", match);
break;
}
});
return { entity, tags };
}
/**
* Builds a comparison function for sorting by similarity to a provided term.
* Intended for sorting typeahead results.
*/
/*
Example:
term: 'tre'
"Treasure of the Sierra Madre" => 26 (starts with, case mismatch)
"one tree hill" => 15 (includes, start of word, case match)
*/
function createMatchinessComparator(term, converter = item => item.toString()) {
const locale = Intl.Collator().resolvedOptions().locale;
const pattern = {
startsWith: '^' + term,
startsWord: '\\b' + term,
};
const regex = {
startsWith: new RegExp(pattern.startsWith),
startsWithI: new RegExp(pattern.startsWith, 'i'),
startsWord: new RegExp(pattern.startsWord),
startsWordI: new RegExp(pattern.startsWord, 'i'),
includes: new RegExp(term),
includesI: new RegExp(term, 'i'),
};
// assign a score based on how well the value matches the search term
const computeMatchiness = (value) => {
switch (true) {
// exact match
case value === term: return 30;
// close match, just varying by accents and/or case
case value.localeCompare(term, locale, { usage: 'search', sensitivity: 'variant' }) === 0: return 28;
case value.localeCompare(term, locale, { usage: 'search', sensitivity: 'accent' }) === 0: return 27;
case value.localeCompare(term, locale, { usage: 'search', sensitivity: 'case' }) === 0: return 26;
case value.localeCompare(term, locale, { usage: 'search', sensitivity: 'base' }) === 0: return 25;
// starts with (including case-insensitive)
case regex.startsWith.test(value): return 20;
case regex.startsWithI.test(value): return 18;
// includes at the start of a word (including case-insensitive)
case regex.startsWord.test(value): return 15;
case regex.startsWordI.test(value): return 13;
// includes anywhere (including case-insensitive)
case regex.includes.test(value): return 10;
case regex.includesI.test(value): return 9;
// no match
default: return 0;
}
};
return (a, b) => {
const textA = converter(a);
const textB = converter(b);
const scoreA = computeMatchiness(textA);
const scoreB = computeMatchiness(textB);
const relativeMatchiness = Math.sign(scoreB - scoreA);
// sort by score, then alphabetically when equal
// localeCompare impls may not be 1|0|-1 only
return relativeMatchiness || textA.localeCompare(textB);
};
}
const Uri = {
rootUri: 'https://app.kanka.io',
route: window.location.pathname,
buildUri: (...segments) => [Uri.rootUri, 'w', Session.campaignID, ...segments].join('/'),
getEditUri: () => document.querySelector('a[href$=edit]').getAttribute('href'),
getEntityUri: () => document.querySelector('head link[rel=canonical]').getAttribute('href'),
};
const Session = {
csrfToken: (_a = document.head.querySelector('meta[name="csrf-token"]')) === null || _a === void 0 ? void 0 : _a.getAttribute('content'),
campaignID: (_b = Uri.route.match(/w\/(?<id>\d+)\//).groups.id) !== null && _b !== void 0 ? _b : '0',
};
const entityBits = Uri.getEntityUri().match(/w\/\d+\/entities\/(?<id>\d+)/);
const editBits = Uri.getEditUri().match(/\/(?<type>\w+)\/(?<id>\d+)\/edit$/);
const Entity = {
/**
* this is the plural, not values from EntityType
*/
entityType: editBits.groups.type,
/**
* this is the 'larger' ID: entities/__[5328807]__ === characters/1357612
*/
entityID: entityBits.groups.id,
/**
* this is the 'smaller' ID: entities/5328807 === characters/__[1357612]__
*/
typedID: editBits.groups.id,
meta: parseBodyClasses(document.body),
};
const EntityTypeAttributes = {
/**
* this encapsulates the definitions from the system
* - some entities have a location, some don't
* - some entities have a link in the header, some use the sidebar
* - some entities can have multiple locations, some can't
*/
hasLocation: ({
default: {},
character: { headerLink: true },
location: { headerLink: true },
map: { headerLink: true },
organisation: { sidebarLink: true },
family: { headerLink: true },
creature: { sidebarLink: true, multiple: true },
race: { sidebarLink: true, multiple: true },
event: { sidebarLink: true },
journal: { sidebarLink: true },
item: { sidebarLink: true },
tag: {},
note: {},
quest: {},
}),
};
const Util = {
createMatchinessComparator,
getElementPromise,
parseBodyClasses,
};
/* unused harmony default export */ var __WEBPACK_DEFAULT_EXPORT__ = ({
Uri,
Session,
Entity,
EntityTypeAttributes,
Util,
Api,
});
/******/ })()
;