Kanka SDK (dev)

Tools for Kanking.

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/508835/1449206/Kanka%20SDK%20%28dev%29.js

  1. // ==UserScript==
  2. // @name Kanka SDK (dev)
  3. // @namespace https://gf.qytechs.cn/en/users/1029479-infinitegeek
  4. // @version 0.0.1-3
  5. // @description Tools for Kanking.
  6. // @author InfiniteGeek
  7. // @supportURL Infinite @ https://discord.gg/rhsyZJ4
  8. // @license MIT
  9. // @match https://app.kanka.io/w/*
  10. // @icon https://www.google.com/s2/favicons?domain=kanka.io
  11. // @keywords kanka,sdk
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. /******/ (() => { // webpackBootstrap
  16. /******/ "use strict";
  17.  
  18. var _a, _b;
  19. const emit_debug = (...args) => { };
  20. //const emit_debug = console.log;
  21. function getElementPromise(...selectorChain) {
  22. let intervalHandle;
  23. let doc;
  24. return new Promise((resolve, reject) => {
  25. const getElement = () => {
  26. if (!jQuery)
  27. return undefined;
  28. try {
  29. let lmnt = (doc !== null && doc !== void 0 ? doc : (doc = jQuery(document.documentElement)));
  30. const selectors = [...selectorChain];
  31. let selector = null;
  32. while (selector = selectors.shift()) {
  33. lmnt = lmnt.find(selector);
  34. if (!lmnt)
  35. return undefined;
  36. }
  37. if (!lmnt)
  38. return null;
  39. intervalHandle && clearInterval(intervalHandle);
  40. resolve(lmnt);
  41. return lmnt;
  42. }
  43. catch (error) {
  44. intervalHandle && clearInterval(intervalHandle);
  45. reject(error);
  46. return null;
  47. }
  48. };
  49. if (typeof MutationObserver) {
  50. // if we have the MutationObserver API, hook to document changes
  51. const observer = new MutationObserver(() => getElement() && observer.disconnect());
  52. observer.observe(document.documentElement, { childList: true, subtree: true });
  53. }
  54. else {
  55. // if not, use a sad timer
  56. intervalHandle = setInterval(getElement, 333);
  57. }
  58. });
  59. }
  60. const Api = {
  61. getXMLHttpRequest: (method) => {
  62. var xhr = new XMLHttpRequest();
  63. xhr.withCredentials = true;
  64. xhr.open(method, Uri.buildUri(Entity.entityType, Entity.typedID), false);
  65. Api.headers.setCsrf(xhr);
  66. Api.headers.setXMLHttpRequest(xhr);
  67. return xhr;
  68. },
  69. headers: {
  70. setCsrf: (xhr) => xhr.setRequestHeader('x-csrf-token', Session.csrfToken),
  71. setXMLHttpRequest: (xhr) => xhr.setRequestHeader('x-requested-with', 'XMLHttpRequest'),
  72. },
  73. createPostParams: () => {
  74. const params = new URLSearchParams();
  75. params.append('_token', Session.csrfToken);
  76. params.append('datagrid-action', 'batch');
  77. // this needs the plural
  78. params.append('entity', Entity.entityType);
  79. params.append('mode', 'table');
  80. // typedID is different from entityID
  81. params.append('models', Entity.typedID);
  82. params.append('undefined', '');
  83. return params;
  84. },
  85. fetch_success: async (response) => {
  86. var _a;
  87. emit_debug('Success:', response);
  88. window.showToast(response.statusText, 'bg-success text-success-content');
  89. return { ok: response.ok, document: (_a = $.parseHTML(await response.text())) !== null && _a !== void 0 ? _a : [] };
  90. },
  91. post: (url, body) => {
  92. return fetch(url, {
  93. method: 'POST',
  94. redirect: 'follow',
  95. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  96. body,
  97. })
  98. .then(Api.fetch_success)
  99. .catch((error) => {
  100. console.error('Error:', error);
  101. window.showToast(error, 'bg-primary text-error-content');
  102. return { ok: false, document: [], error };
  103. });
  104. }
  105. };
  106. /**
  107. * Extract metadata from the classes on the <body>
  108. */
  109. function parseBodyClasses(body) {
  110. const classes = Array.from(body.classList);
  111. const entity = { id: '', entityType: 'default', type: '' };
  112. const tags = [];
  113. const kankaClassRegex = /^kanka-(\w+)-(\w+)$/;
  114. let tempTag = null;
  115. function processTag(isValueNumeric, value) {
  116. // tags are emitted as id/name pairs
  117. // parent tags also end up in the list as ID-only entries
  118. // any name is associated with the ID prior
  119. if (isValueNumeric) {
  120. tempTag = value;
  121. }
  122. else if (tempTag !== null) {
  123. tags.push({ id: tempTag, entityType: value });
  124. tempTag = null;
  125. }
  126. }
  127. classes
  128. .map(className => className.match(kankaClassRegex))
  129. .filter(match => !!match)
  130. .forEach((match) => {
  131. const [, key, value] = match;
  132. const isValueNumeric = !isNaN(Number(value));
  133. switch (key) {
  134. // kanka-entity-{entityID} kanka-entity-{entityType}
  135. case 'entity':
  136. if (isValueNumeric) {
  137. entity['id'] = value;
  138. }
  139. else {
  140. entity['entityType'] = value;
  141. }
  142. break;
  143. // kanka-type-{typeValue}
  144. case 'type':
  145. entity.type = value;
  146. break;
  147. // kanka-tag-{id} kanka-tag-{name}
  148. case 'tag':
  149. processTag(isValueNumeric, value);
  150. break;
  151. default:
  152. console.warn("What's this? 💀🎃", match);
  153. break;
  154. }
  155. });
  156. return { entity, tags };
  157. }
  158. /**
  159. * Builds a comparison function for sorting by similarity to a provided term.
  160. * Intended for sorting typeahead results.
  161. */
  162. /*
  163. Example:
  164. term: 'tre'
  165. "Treasure of the Sierra Madre" => 26 (starts with, case mismatch)
  166. "one tree hill" => 15 (includes, start of word, case match)
  167. */
  168. function createMatchinessComparator(term, converter = item => item.toString()) {
  169. const locale = Intl.Collator().resolvedOptions().locale;
  170. const pattern = {
  171. startsWith: '^' + term,
  172. startsWord: '\\b' + term,
  173. };
  174. const regex = {
  175. startsWith: new RegExp(pattern.startsWith),
  176. startsWithI: new RegExp(pattern.startsWith, 'i'),
  177. startsWord: new RegExp(pattern.startsWord),
  178. startsWordI: new RegExp(pattern.startsWord, 'i'),
  179. includes: new RegExp(term),
  180. includesI: new RegExp(term, 'i'),
  181. };
  182. // assign a score based on how well the value matches the search term
  183. const computeMatchiness = (value) => {
  184. switch (true) {
  185. // exact match
  186. case value === term: return 30;
  187. // close match, just varying by accents and/or case
  188. case value.localeCompare(term, locale, { usage: 'search', sensitivity: 'variant' }) === 0: return 28;
  189. case value.localeCompare(term, locale, { usage: 'search', sensitivity: 'accent' }) === 0: return 27;
  190. case value.localeCompare(term, locale, { usage: 'search', sensitivity: 'case' }) === 0: return 26;
  191. case value.localeCompare(term, locale, { usage: 'search', sensitivity: 'base' }) === 0: return 25;
  192. // starts with (including case-insensitive)
  193. case regex.startsWith.test(value): return 20;
  194. case regex.startsWithI.test(value): return 18;
  195. // includes at the start of a word (including case-insensitive)
  196. case regex.startsWord.test(value): return 15;
  197. case regex.startsWordI.test(value): return 13;
  198. // includes anywhere (including case-insensitive)
  199. case regex.includes.test(value): return 10;
  200. case regex.includesI.test(value): return 9;
  201. // no match
  202. default: return 0;
  203. }
  204. };
  205. return (a, b) => {
  206. const textA = converter(a);
  207. const textB = converter(b);
  208. const scoreA = computeMatchiness(textA);
  209. const scoreB = computeMatchiness(textB);
  210. const relativeMatchiness = Math.sign(scoreB - scoreA);
  211. // sort by score, then alphabetically when equal
  212. // localeCompare impls may not be 1|0|-1 only
  213. return relativeMatchiness || textA.localeCompare(textB);
  214. };
  215. }
  216. const Uri = {
  217. rootUri: 'https://app.kanka.io',
  218. route: window.location.pathname,
  219. buildUri: (...segments) => [Uri.rootUri, 'w', Session.campaignID, ...segments].join('/'),
  220. getEditUri: () => document.querySelector('a[href$=edit]').getAttribute('href'),
  221. getEntityUri: () => document.querySelector('head link[rel=canonical]').getAttribute('href'),
  222. };
  223. const Session = {
  224. csrfToken: (_a = document.head.querySelector('meta[name="csrf-token"]')) === null || _a === void 0 ? void 0 : _a.getAttribute('content'),
  225. campaignID: (_b = Uri.route.match(/w\/(?<id>\d+)\//).groups.id) !== null && _b !== void 0 ? _b : '0',
  226. };
  227. const entityBits = Uri.getEntityUri().match(/w\/\d+\/entities\/(?<id>\d+)/);
  228. const editBits = Uri.getEditUri().match(/\/(?<type>\w+)\/(?<id>\d+)\/edit$/);
  229. const Entity = {
  230. /**
  231. * this is the plural, not values from EntityType
  232. */
  233. entityType: editBits.groups.type,
  234. /**
  235. * this is the 'larger' ID: entities/__[5328807]__ === characters/1357612
  236. */
  237. entityID: entityBits.groups.id,
  238. /**
  239. * this is the 'smaller' ID: entities/5328807 === characters/__[1357612]__
  240. */
  241. typedID: editBits.groups.id,
  242. meta: parseBodyClasses(document.body),
  243. };
  244. const EntityTypeAttributes = {
  245. /**
  246. * this encapsulates the definitions from the system
  247. * - some entities have a location, some don't
  248. * - some entities have a link in the header, some use the sidebar
  249. * - some entities can have multiple locations, some can't
  250. */
  251. hasLocation: ({
  252. default: {},
  253. character: { headerLink: true },
  254. location: { headerLink: true },
  255. map: { headerLink: true },
  256. organisation: { sidebarLink: true },
  257. family: { headerLink: true },
  258. creature: { sidebarLink: true, multiple: true },
  259. race: { sidebarLink: true, multiple: true },
  260. event: { sidebarLink: true },
  261. journal: { sidebarLink: true },
  262. item: { sidebarLink: true },
  263. tag: {},
  264. note: {},
  265. quest: {},
  266. }),
  267. };
  268. const Util = {
  269. createMatchinessComparator,
  270. getElementPromise,
  271. parseBodyClasses,
  272. };
  273. /* unused harmony default export */ var __WEBPACK_DEFAULT_EXPORT__ = ({
  274. Uri,
  275. Session,
  276. Entity,
  277. EntityTypeAttributes,
  278. Util,
  279. Api,
  280. });
  281.  
  282. /******/ })()
  283. ;

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址