Files
rd.rdlevel.cn/script/custommethod-search.js
T
2026-10-05 18:55:40 +08:00

712 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
(function () {
"use strict";
let ParameterType;
(function (ParameterType) {
ParameterType["String"] = "string";
ParameterType["Number"] = "number";
ParameterType["Boolean"] = "boolean";
ParameterType["Enum"] = "enum";
})(ParameterType || (ParameterType = {}));
let MethodType;
(function (MethodType) {
MethodType["Mod"] = "mod";
MethodType["Surgery"] = "surgery";
MethodType["Comment"] = "comment";
MethodType["CustomMethod"] = "custom_method";
MethodType["Ink"] = "ink";
MethodType["Variable"] = "variable";
MethodType["Condition"] = "condition";
MethodType["CustomClass"] = "custom_class";
})(MethodType || (MethodType = {}));
const enumTypeMap = {
Characters: ["Logan", "Green", "Blue", "Yellow", "Purple", "Orange"],
Size: ["Small", "Medium", "Large"],
Shape: ["Circle", "Square", "Triangle"],
};
class NumericRange {
constructor(min, max, isInteger = false) {
this.min = min;
this.max = max;
this.isInteger = isInteger;
}
}
class Parameter {
constructor(name, type, description, enumType, numericRange) {
this.name = name;
this.type = type;
this.description = description;
this.enumType = enumType;
this.numericRange = numericRange;
}
}
class Method {
constructor(name, type, shortDescription, description) {
this.name = name;
this.type = type;
this.shortDescription = shortDescription;
this.description = description;
}
}
class Mod extends Method {
constructor(name, shortDescription, description) {
super(name, MethodType.Mod, shortDescription, description);
}
}
class Surgery extends Method {
constructor(name, shortDescription, description) {
super(name, MethodType.Surgery, shortDescription, description);
}
}
class CommentMethod extends Method {
constructor(name, shortDescription, description, parameters) {
super(name, MethodType.Comment, shortDescription, description);
this.parameters = parameters;
}
}
class InkMethod extends Method {
constructor(name, shortDescription, description, parameters) {
super(name, MethodType.Ink, shortDescription, description);
this.parameters = parameters;
}
}
class CustomMethods extends Method {
constructor(name, shortDescription, description, parameters) {
super(name, MethodType.CustomMethod, shortDescription, description);
this.parameters = parameters;
}
}
class VariableMethod extends Method {
constructor(name, shortDescription, description, parameters) {
super(name, MethodType.Variable, shortDescription, description);
this.parameters = parameters;
}
}
class ConditionMethod extends Method {
constructor(name, shortDescription, description, parameters) {
super(name, MethodType.Condition, shortDescription, description);
this.parameters = parameters;
}
}
class CustomClassMethod extends Method {
constructor(name, shortDescription, description) {
super(name, MethodType.CustomClass, shortDescription, description);
}
}
function editDistance(a, b, caseSensitive = true) {
if (a.length === 0)
return b.length;
if (b.length === 0)
return a.length;
const matrix = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
const ac = a.charAt(j - 1);
const bc = b.charAt(i - 1);
if (caseSensitive ? bc === ac : bc.toLowerCase() === ac.toLowerCase()) {
matrix[i][j] = matrix[i - 1][j - 1];
}
else {
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, Math.min(matrix[i][j - 1] + 1, matrix[i - 1][j] + 1));
}
}
}
return matrix[b.length][a.length];
}
function parametersOf(method) {
var _a;
return (_a = method.parameters) !== null && _a !== void 0 ? _a : [];
}
// 全局策略:字符串写法、数字/布尔默认的常量/表达式状态
const strategies = {
string: "prefix",
number: "constant",
boolean: "constant",
};
// ---------------------------------------------------------------------
// 外置数据 hydrate(script/custommethod-data.js)
// ---------------------------------------------------------------------
function hydrateMethod(raw) {
var _a, _b, _c, _d;
const shortDescription = (_b = (_a = raw.shortDescription) !== null && _a !== void 0 ? _a : raw.description) !== null && _b !== void 0 ? _b : "";
const description = (_d = (_c = raw.description) !== null && _c !== void 0 ? _c : raw.shortDescription) !== null && _d !== void 0 ? _d : "";
const parameters = Array.isArray(raw.parameters)
? raw.parameters.map((item) => {
var _a, _b;
return new Parameter(item.name, (_a = item.type) !== null && _a !== void 0 ? _a : ParameterType.String, (_b = item.description) !== null && _b !== void 0 ? _b : "", item.enumType, item.numericRange
? new NumericRange(item.numericRange.min, item.numericRange.max, item.numericRange.isInteger)
: undefined);
})
: [];
switch (raw.type) {
case MethodType.Mod: return new Mod(raw.name, shortDescription, description);
case MethodType.Surgery: return new Surgery(raw.name, shortDescription, description);
case MethodType.Comment: return new CommentMethod(raw.name, shortDescription, description, parameters);
case MethodType.Ink: return new InkMethod(raw.name, shortDescription, description, parameters);
case MethodType.Variable: return new VariableMethod(raw.name, shortDescription, description, parameters);
case MethodType.Condition: return new ConditionMethod(raw.name, shortDescription, description, parameters);
case MethodType.CustomClass: return new CustomClassMethod(raw.name, shortDescription, description);
default: return new CustomMethods(raw.name, shortDescription, description, parameters);
}
}
let methods = [];
function loadExternalData() {
const data = window.CustomMethodSearchData;
if (!data)
return;
if (data.enumTypes) {
for (const key of Object.keys(data.enumTypes)) {
enumTypeMap[key] = data.enumTypes[key];
}
}
if (Array.isArray(data.methods)) {
const index = new Map();
methods.forEach((method, i) => index.set(method.name.toLowerCase(), i));
for (const raw of data.methods) {
if (!raw || !raw.name)
continue;
const name = String(raw.name).toLowerCase();
const hydrated = hydrateMethod(raw);
if (index.has(name)) {
methods[index.get(name)] = hydrated;
}
else {
index.set(name, methods.length);
methods.push(hydrated);
}
}
}
}
loadExternalData();
// ---------------------------------------------------------------------
// 匹配
// ---------------------------------------------------------------------
function searchMethods(query) {
const q = query.trim();
if (!q)
return [];
const ql = q.toLowerCase();
const maxDistance = Math.max(2, Math.floor(q.length / 2));
return methods
.map((method) => {
const name = method.name;
const lower = name.toLowerCase();
let score;
if (name === q) {
score = 0;
}
else if (lower.startsWith(ql)) {
score = 1;
}
else if (lower.indexOf(ql) >= 0) {
score = 2;
}
else {
score = 3 + editDistance(name, q, false);
}
return { method, score };
})
.filter((item) => item.score <= 2 || item.score <= 3 + maxDistance)
.sort((a, b) => a.score - b.score || a.method.name.localeCompare(b.method.name))
.slice(0, 8)
.map((item) => item.method);
}
// ---------------------------------------------------------------------
// DOM 工具
// ---------------------------------------------------------------------
function el(tag, className) {
const node = document.createElement(tag);
if (className)
node.className = className;
return node;
}
function resolveEnumOptions(parameter) {
if (parameter.enumType && enumTypeMap[parameter.enumType])
return enumTypeMap[parameter.enumType];
if (enumTypeMap[parameter.description])
return enumTypeMap[parameter.description];
if (enumTypeMap[parameter.name])
return enumTypeMap[parameter.name];
return [];
}
// 布尔可用 true/false,或布尔变量 b0~b9
const BOOLEAN_OPTIONS = ["true", "false"];
for (let i = 0; i < 10; i++) {
BOOLEAN_OPTIONS.push("b" + i);
}
function makeCombo(parameter, options, onChange) {
const combo = el("span", "cm-combo");
const input = el("input", "cm-input cm-combo-input");
input.type = "text";
input.placeholder = parameter.name;
input.autocomplete = "off";
const list = el("ul", "cm-combo-list");
list.hidden = true;
function populate() {
list.textContent = "";
if (options.length === 0) {
list.hidden = true;
return;
}
const query = input.value.trim();
const ql = query.toLowerCase();
const maxDistance = Math.max(1, Math.floor(query.length / 2));
const shown = (query
? options.filter((option) => option.toLowerCase().indexOf(ql) >= 0 ||
editDistance(option, query, false) <= maxDistance)
: options).slice(0, 10);
if (shown.length === 0) {
list.hidden = true;
return;
}
for (const option of shown) {
const item = el("li", "cm-combo-item");
item.textContent = option;
item.title = option;
item.addEventListener("mousedown", (event) => {
event.preventDefault();
input.value = option;
list.hidden = true;
onChange();
});
list.appendChild(item);
}
list.hidden = false;
}
input.addEventListener("input", () => { populate(); onChange(); });
input.addEventListener("focus", populate);
input.addEventListener("blur", () => { window.setTimeout(() => { list.hidden = true; }, 120); });
combo.appendChild(input);
combo.appendChild(list);
return combo;
}
function makeModeToggle(initial, onChange) {
const button = el("button", "cm-mode");
button.type = "button";
let mode = initial;
const render = () => {
button.textContent = mode === "expression" ? "表达式" : "常量";
button.title = "切换 常量 / 表达式";
button.classList.toggle("is-expression", mode === "expression");
};
render();
button.addEventListener("click", (event) => {
event.preventDefault();
mode = mode === "expression" ? "constant" : "expression";
render();
onChange();
});
return { el: button, get: () => mode };
}
function stringValue(value) {
if (value.length === 0)
return "<文本>";
return strategies.string === "quote" ? `"${value}"` : `str:${value}`;
}
function makeField(parameter, onChange) {
switch (parameter.type) {
case ParameterType.Boolean: {
const wrap = el("span", "cm-field");
const checkbox = el("input", "cm-input cm-check");
checkbox.type = "checkbox";
checkbox.addEventListener("change", onChange);
const combo = makeCombo(parameter, BOOLEAN_OPTIONS, onChange);
const comboInput = combo.querySelector("input");
let mode = strategies.boolean;
const toggle = makeModeToggle(strategies.boolean, () => {
mode = toggle.get();
applyMode();
onChange();
});
function applyMode() {
const expression = mode === "expression";
checkbox.hidden = expression;
combo.hidden = !expression;
}
applyMode();
wrap.appendChild(toggle.el);
wrap.appendChild(checkbox);
wrap.appendChild(combo);
return {
el: wrap,
read: () => {
if (mode === "constant") {
return { text: checkbox.checked ? "true" : "false", valid: true, invalid: false };
}
const value = comboInput.value.trim();
return value.length > 0
? { text: value, valid: true, invalid: false }
: { text: "<布尔>", valid: false, invalid: true };
},
};
}
case ParameterType.Number: {
const wrap = el("span", "cm-field");
const input = el("input", "cm-input");
input.type = "text";
input.inputMode = "decimal";
input.placeholder = parameter.name;
input.addEventListener("input", onChange);
const mode = makeModeToggle(strategies.number, onChange);
wrap.appendChild(mode.el);
wrap.appendChild(input);
return {
el: wrap,
read: () => {
const value = input.value.trim();
if (value.length === 0)
return { text: "<数字>", valid: false, invalid: true };
if (mode.get() === "expression")
return { text: value, valid: true, invalid: false };
const number = Number(value);
let valid = isFinite(number);
const range = parameter.numericRange;
if (valid && range) {
if (range.isInteger && !Number.isInteger(number))
valid = false;
if (range.min !== undefined && number < range.min)
valid = false;
if (range.max !== undefined && number > range.max)
valid = false;
}
return { text: value, valid, invalid: !valid };
},
};
}
case ParameterType.Enum: {
const wrap = el("span", "cm-field cm-field-string");
const open = el("span", "cm-lit");
const close = el("span", "cm-lit");
open.textContent = strategies.string === "quote" ? "\"" : "str:";
close.textContent = strategies.string === "quote" ? "\"" : "";
const combo = makeCombo(parameter, resolveEnumOptions(parameter), onChange);
const input = combo.querySelector("input");
wrap.appendChild(open);
wrap.appendChild(combo);
wrap.appendChild(close);
return {
el: wrap,
read: () => {
const value = input.value.trim();
const options = resolveEnumOptions(parameter);
const valid = options.length > 0
? options.some((option) => option.toLowerCase() === value.toLowerCase())
: value.length > 0;
return { text: stringValue(value), valid, invalid: !valid };
},
};
}
default: {
const wrap = el("span", "cm-field cm-field-string");
const open = el("span", "cm-lit");
const close = el("span", "cm-lit");
open.textContent = strategies.string === "quote" ? "\"" : "str:";
close.textContent = strategies.string === "quote" ? "\"" : "";
const input = el("input", "cm-input");
input.type = "text";
input.placeholder = parameter.name;
input.addEventListener("input", onChange);
wrap.appendChild(open);
wrap.appendChild(input);
wrap.appendChild(close);
return {
el: wrap,
read: () => {
const value = input.value.trim();
return { text: stringValue(value), valid: value.length > 0, invalid: value.length === 0 };
},
};
}
}
}
function methodSkeleton(method, hasParams) {
switch (method.type) {
case MethodType.Comment: return { prefix: `()=>${method.name}(`, suffix: ")" };
case MethodType.Ink: return { prefix: `DOTHIS{${method.name}(`, suffix: ")}" };
case MethodType.Variable: return { prefix: `${method.name} = `, suffix: "" };
case MethodType.Mod: return { prefix: `"mods": [..., "`, suffix: `"]` };
case MethodType.CustomClass: return { prefix: `"customClass": "`, suffix: `"` };
case MethodType.Condition:
return hasParams ? { prefix: `${method.name}(`, suffix: ")" } : { prefix: method.name, suffix: "" };
case MethodType.Surgery: return { prefix: method.name, suffix: "" };
default:
return hasParams ? { prefix: `${method.name}(`, suffix: ")" } : { prefix: method.name, suffix: "" };
}
}
function formatMethod(method, states) {
const args = states.map((state) => state.text).join(", ");
switch (method.type) {
case MethodType.Comment: return `()=>${method.name}(${args})`;
case MethodType.Ink: return `DOTHIS{${method.name}(${args})}`;
case MethodType.Variable: return `${method.name} = ${states.length ? states[0].text : ""}`;
case MethodType.Mod: return `"mods": [..., "${method.name}"]`;
case MethodType.CustomClass: return `"customClass": "${method.name}"`;
case MethodType.Condition: return states.length ? `${method.name}(${args})` : method.name;
case MethodType.Surgery: return method.name;
default: return states.length ? `${method.name}(${args})` : method.name;
}
}
function copyText(text) {
return __awaiter(this, void 0, void 0, function* () {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
yield navigator.clipboard.writeText(text);
return true;
}
}
catch (error) {
// fall through
}
try {
const area = document.createElement("textarea");
area.value = text;
area.style.position = "fixed";
area.style.opacity = "0";
document.body.appendChild(area);
area.select();
const ok = document.execCommand("copy");
document.body.removeChild(area);
return ok;
}
catch (error) {
return false;
}
});
}
// ---------------------------------------------------------------------
// 渲染
// ---------------------------------------------------------------------
function renderContainer(container) {
if (container.dataset.cmReady === "1")
return;
container.dataset.cmReady = "1";
let currentMethod = null;
let query = container.querySelector(".cm-query");
let suggest = container.querySelector(".cm-suggest");
let selected = container.querySelector(".cm-selected");
let nameEl = container.querySelector(".cm-name");
let shortEl = container.querySelector(".cm-short");
let call = container.querySelector(".cm-call");
let preview = container.querySelector(".cm-preview");
let desc = container.querySelector(".cm-desc");
let options = container.querySelector(".cm-options");
if (!query || !suggest || !selected || !nameEl || !shortEl || !call || !preview || !desc) {
container.textContent = "";
const searchBar = el("div", "cm-search");
query = el("input", "cm-query");
query.type = "text";
query.placeholder = "输入自定义方法名,如 ChangeCharacter";
query.autocomplete = "off";
suggest = el("ul", "cm-suggest");
searchBar.appendChild(query);
searchBar.appendChild(suggest);
selected = el("div", "cm-selected");
selected.hidden = true;
const head = el("div", "cm-head");
nameEl = el("span", "cm-name");
shortEl = el("span", "cm-short");
head.appendChild(nameEl);
head.appendChild(shortEl);
call = el("div", "cm-call");
preview = el("code", "cm-preview");
desc = el("div", "cm-desc");
selected.appendChild(head);
selected.appendChild(call);
selected.appendChild(preview);
selected.appendChild(desc);
container.appendChild(searchBar);
container.appendChild(selected);
}
// 搜索框下的策略选项
options = container.querySelector(".cm-options");
if (!options) {
options = el("div", "cm-options");
query.parentElement.insertAdjacentElement("afterend", options);
}
if (!options.querySelector(".cm-option-select")) {
const optionsBox = options;
function addOption(label, key, values, current) {
const wrap = el("label", "cm-option");
const text = el("span", "cm-option-label");
text.textContent = label;
const select = el("select", "cm-option-select");
for (const [value, caption] of values) {
const opt = el("option");
opt.value = value;
opt.textContent = caption;
select.appendChild(opt);
}
select.value = current;
select.addEventListener("change", () => {
strategies[key] = select.value;
if (currentMethod) {
selectMethod(currentMethod);
}
});
wrap.appendChild(text);
wrap.appendChild(select);
optionsBox.appendChild(wrap);
}
addOption("字符串", "string", [["prefix", "str: 前缀"], ["quote", "引号 \"…\""]], strategies.string);
}
const queryEl = query;
const suggestEl = suggest;
const selectedEl = selected;
const nameNode = nameEl;
const shortNode = shortEl;
const callEl = call;
const previewEl = preview;
const descNode = desc;
const copyBtn = el("button", "cm-copy");
copyBtn.type = "button";
copyBtn.textContent = "复制";
copyBtn.hidden = true;
copyBtn.title = "复制生成的代码";
selectedEl.appendChild(copyBtn);
copyBtn.addEventListener("click", () => {
var _a;
copyText((_a = previewEl.textContent) !== null && _a !== void 0 ? _a : "").then((ok) => {
copyBtn.textContent = ok ? "已复制" : "复制失败";
window.setTimeout(() => { copyBtn.textContent = "复制"; }, 1200);
});
});
function clearSuggest() {
suggestEl.textContent = "";
}
function selectMethod(method) {
currentMethod = method;
clearSuggest();
queryEl.value = method.name;
selectedEl.hidden = false;
nameNode.textContent = method.name;
shortNode.textContent = method.shortDescription || "";
descNode.textContent = method.description || "";
callEl.textContent = "";
previewEl.textContent = "";
copyBtn.hidden = true;
const parameters = parametersOf(method);
const fields = [];
function updatePreview() {
const states = fields.map((field) => field.read());
fields.forEach((field, i) => field.el.classList.toggle("cm-invalid", states[i].invalid));
previewEl.textContent = formatMethod(method, states);
copyBtn.hidden = !states.every((state) => state.valid);
}
const skeleton = methodSkeleton(method, parameters.length > 0);
const code = el("code", "cm-code");
const prefix = el("span", "cm-lit");
prefix.textContent = skeleton.prefix;
code.appendChild(prefix);
parameters.forEach((parameter, index) => {
if (index > 0) {
const sep = el("span", "cm-lit");
sep.textContent = ", ";
code.appendChild(sep);
}
const field = makeField(parameter, updatePreview);
field.el.title = parameter.description;
code.appendChild(field.el);
fields.push(field);
});
if (skeleton.suffix) {
const suffix = el("span", "cm-lit");
suffix.textContent = skeleton.suffix;
code.appendChild(suffix);
}
callEl.appendChild(code);
if (parameters.length === 0) {
const empty = el("span", "cm-empty");
empty.textContent = "(无参数)";
callEl.appendChild(empty);
}
updatePreview();
}
function updateSuggestions() {
clearSuggest();
for (const method of searchMethods(queryEl.value)) {
const item = el("li", "cm-suggest-item");
const nameSpan = el("span", "cm-suggest-name");
nameSpan.textContent = method.name;
const descSpan = el("span", "cm-suggest-desc");
descSpan.textContent = method.shortDescription || method.description;
item.appendChild(nameSpan);
item.appendChild(descSpan);
item.addEventListener("mousedown", (event) => {
event.preventDefault();
selectMethod(method);
});
suggestEl.appendChild(item);
}
}
queryEl.addEventListener("input", () => {
updateSuggestions();
selectedEl.hidden = true;
});
queryEl.addEventListener("focus", updateSuggestions);
queryEl.addEventListener("blur", () => {
window.setTimeout(clearSuggest, 120);
});
if (queryEl.value) {
updateSuggestions();
}
}
function initAll() {
document.querySelectorAll(".custommethods-search").forEach((container) => renderContainer(container));
}
function register() {
const docsify = window.$docsify;
if (docsify) {
docsify.plugins = [].concat(docsify.plugins, (hook) => {
hook.doneEach(() => initAll());
});
}
else if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => initAll());
}
else {
initAll();
}
}
window.CustomMethodSearch = {
search: searchMethods,
editDistance,
renderAll: initAll,
strategies,
getMethods: () => methods,
setMethods: (list) => { methods = list; initAll(); },
Parameter,
ParameterType,
Method,
MethodType,
Mod,
Surgery,
CommentMethod,
InkMethod,
CustomMethods,
VariableMethod,
ConditionMethod,
CustomClassMethod,
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", register);
}
else {
register();
}
})();