(function () { "use strict"; enum ParameterType { String = "string", Number = "number", Boolean = "boolean", Enum = "enum", } enum MethodType { Mod = "mod", Surgery = "surgery", Comment = "comment", CustomMethod = "custom_method", Ink = "ink", Variable = "variable", Condition = "condition", CustomClass = "custom_class", } type ValueMode = "constant" | "expression"; type StringStrategy = "prefix" | "quote"; const enumTypeMap: Record = { Characters: ["Logan", "Green", "Blue", "Yellow", "Purple", "Orange"], Size: ["Small", "Medium", "Large"], Shape: ["Circle", "Square", "Triangle"], }; class NumericRange { min?: number; max?: number; isInteger: boolean; public constructor(min?: number, max?: number, isInteger: boolean = false) { this.min = min; this.max = max; this.isInteger = isInteger; } } class Parameter { name: string; type: ParameterType; enumType?: string; numericRange?: NumericRange; description: string; public constructor(name: string, type: ParameterType, description: string, enumType?: string, numericRange?: NumericRange) { this.name = name; this.type = type; this.description = description; this.enumType = enumType; this.numericRange = numericRange; } } class Method { name: string; type: MethodType; shortDescription: string; description: string; public constructor(name: string, type: MethodType, shortDescription: string, description: string) { this.name = name; this.type = type; this.shortDescription = shortDescription; this.description = description; } } class Mod extends Method { public constructor(name: string, shortDescription: string, description: string) { super(name, MethodType.Mod, shortDescription, description); } } class Surgery extends Method { public constructor(name: string, shortDescription: string, description: string) { super(name, MethodType.Surgery, shortDescription, description); } } class CommentMethod extends Method { parameters: Parameter[]; public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) { super(name, MethodType.Comment, shortDescription, description); this.parameters = parameters; } } class InkMethod extends Method { parameters: Parameter[]; public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) { super(name, MethodType.Ink, shortDescription, description); this.parameters = parameters; } } class CustomMethods extends Method { parameters: Parameter[]; public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) { super(name, MethodType.CustomMethod, shortDescription, description); this.parameters = parameters; } } class VariableMethod extends Method { parameters: Parameter[]; public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) { super(name, MethodType.Variable, shortDescription, description); this.parameters = parameters; } } class ConditionMethod extends Method { parameters: Parameter[]; public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) { super(name, MethodType.Condition, shortDescription, description); this.parameters = parameters; } } class CustomClassMethod extends Method { public constructor(name: string, shortDescription: string, description: string) { super(name, MethodType.CustomClass, shortDescription, description); } } function editDistance(a: string, b: string, caseSensitive: boolean = true): number { if (a.length === 0) return b.length; if (b.length === 0) return a.length; const matrix: number[][] = []; 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: Method): Parameter[] { return (method as CommentMethod | InkMethod | CustomMethods | VariableMethod | ConditionMethod).parameters ?? []; } // 全局策略:字符串写法、数字/布尔默认的常量/表达式状态 const strategies: { string: StringStrategy; number: ValueMode; boolean: ValueMode } = { string: "prefix", number: "constant", boolean: "constant", }; // --------------------------------------------------------------------- // 外置数据 hydrate(script/custommethod-data.js) // --------------------------------------------------------------------- function hydrateMethod(raw: any): Method { const shortDescription = raw.shortDescription ?? raw.description ?? ""; const description = raw.description ?? raw.shortDescription ?? ""; const parameters: Parameter[] = Array.isArray(raw.parameters) ? raw.parameters.map((item: any) => new Parameter( item.name, item.type ?? ParameterType.String, item.description ?? "", 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: Method[] = []; function loadExternalData(): void { const data = (window as any).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: string): Method[] { 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: number; 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: K, className?: string): HTMLElementTagNameMap[K] { const node = document.createElement(tag); if (className) node.className = className; return node; } function resolveEnumOptions(parameter: Parameter): string[] { 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: string[] = ["true", "false"]; for (let i = 0; i < 10; i++) { BOOLEAN_OPTIONS.push("b" + i); } function makeCombo(parameter: Parameter, options: string[], onChange: () => void): HTMLElement { const combo = el("span", "cm-combo"); const input = el("input", "cm-input cm-combo-input") as HTMLInputElement; input.type = "text"; input.placeholder = parameter.name; input.autocomplete = "off"; const list = el("ul", "cm-combo-list"); list.hidden = true; function populate(): void { 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: ValueMode, onChange: () => void): { el: HTMLButtonElement; get: () => ValueMode } { const button = el("button", "cm-mode") as HTMLButtonElement; button.type = "button"; let mode = initial; const render = (): void => { 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 }; } interface ParamState { text: string; valid: boolean; invalid: boolean; } interface Field { el: HTMLElement; read: () => ParamState; } function stringValue(value: string): string { if (value.length === 0) return "<文本>"; return strategies.string === "quote" ? `"${value}"` : `str:${value}`; } function makeField(parameter: Parameter, onChange: () => void): Field { switch (parameter.type) { case ParameterType.Boolean: { const wrap = el("span", "cm-field"); const checkbox = el("input", "cm-input cm-check") as HTMLInputElement; checkbox.type = "checkbox"; checkbox.addEventListener("change", onChange); const combo = makeCombo(parameter, BOOLEAN_OPTIONS, onChange); const comboInput = combo.querySelector("input") as HTMLInputElement; let mode: ValueMode = strategies.boolean; const toggle = makeModeToggle(strategies.boolean, () => { mode = toggle.get(); applyMode(); onChange(); }); function applyMode(): void { 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") as HTMLInputElement; 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") as HTMLInputElement; 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") as HTMLInputElement; 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 }; }, }; } } } interface Skeleton { prefix: string; suffix: string; } function methodSkeleton(method: Method, hasParams: boolean): Skeleton { 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: Method, states: ParamState[]): string { 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; } } async function copyText(text: string): Promise { try { if (navigator.clipboard && navigator.clipboard.writeText) { await 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: HTMLElement): void { if (container.dataset.cmReady === "1") return; container.dataset.cmReady = "1"; let currentMethod: Method | null = 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") as HTMLInputElement; 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 as HTMLElement).insertAdjacentElement("afterend", options); } if (!options.querySelector(".cm-option-select")) { const optionsBox = options; function addOption(label: string, key: "string" | "number" | "boolean", values: [string, string][], current: string): void { const wrap = el("label", "cm-option"); const text = el("span", "cm-option-label"); text.textContent = label; const select = el("select", "cm-option-select") as HTMLSelectElement; 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 as any)[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") as HTMLButtonElement; copyBtn.type = "button"; copyBtn.textContent = "复制"; copyBtn.hidden = true; copyBtn.title = "复制生成的代码"; selectedEl.appendChild(copyBtn); copyBtn.addEventListener("click", () => { copyText(previewEl.textContent ?? "").then((ok) => { copyBtn.textContent = ok ? "已复制" : "复制失败"; window.setTimeout(() => { copyBtn.textContent = "复制"; }, 1200); }); }); function clearSuggest(): void { suggestEl.textContent = ""; } function selectMethod(method: Method): void { 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: Field[] = []; function updatePreview(): void { 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(): void { 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(): void { document.querySelectorAll(".custommethods-search").forEach((container) => renderContainer(container)); } function register(): void { const docsify = (window as any).$docsify; if (docsify) { docsify.plugins = ([] as any[]).concat(docsify.plugins, (hook: any) => { hook.doneEach(() => initAll()); }); } else if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => initAll()); } else { initAll(); } } (window as any).CustomMethodSearch = { search: searchMethods, editDistance, renderAll: initAll, strategies, getMethods: () => methods, setMethods: (list: Method[]) => { 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(); } })();