Kanka SDK

Tools for Kanking.

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/508838/1449192/Kanka%20SDK.js

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