您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Tools for Kanking.
当前为
此脚本不应直接安装,它是供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.gf.qytechs.cn/scripts/508835/1449187/Kanka%20SDK%20%28dev%29.js
// ==UserScript== // @name Kanka SDK (dev) // @namespace https://gf.qytechs.cn/en/users/1029479-infinitegeek // @version 0.0.1-1 // @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 System.register([], function (exports_1, context_1) { 'use strict'; var emit_debug, Api, Uri, Session, entityBits, editBits, Entity, EntityTypeAttributes, Util, Kanka; var __moduleName = context_1 && context_1.id; //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 ??= 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); } }); } /** * 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); }; } return { setters: [], execute: function () { emit_debug = (...args) => { }; 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) => { emit_debug('Success:', response); window.showToast(response.statusText, 'bg-success text-success-content'); return { ok: response.ok, document: $.parseHTML(await response.text()) ?? [] }; }, 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 }; }); } }; 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'), }; Session = { csrfToken: document.head.querySelector('meta[name="csrf-token"]')?.getAttribute('content'), campaignID: Uri.route.match(/w\/(?<id>\d+)\//).groups.id ?? '0', }; entityBits = Uri.getEntityUri().match(/w\/\d+\/entities\/(?<id>\d+)/); editBits = Uri.getEditUri().match(/\/(?<type>\w+)\/(?<id>\d+)\/edit$/); 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), }; 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: {}, }), }; Util = { createMatchinessComparator, getElementPromise, parseBodyClasses, }; Kanka = { Uri, Session, Entity, EntityTypeAttributes, Util, Api, }; exports_1("default", Kanka); } }; }); /******/ })() ;
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址