diff --git a/src/components/index.ts b/src/components/index.ts index 5dba801..1e2c003 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -20,9 +20,9 @@ export type { TableProps } from './md-table'; export type { BgProps } from './md-bg'; // 导出 md-commander 相关 -export type { - MdCommanderProps, - MdCommanderCommand, +export type { + MdCommanderProps, + MdCommanderCommand, MdCommanderCommandMap, MdCommanderParameter, MdCommanderOption, @@ -39,4 +39,5 @@ export { TabBar } from './md-commander/TabBar'; export { TrackerView } from './md-commander/TrackerView'; export { CommanderEntries } from './md-commander/CommanderEntries'; export { CommanderInput } from './md-commander/CommanderInput'; -export { useCommander, defaultCommands } from './md-commander/hooks'; \ No newline at end of file +export { useCommander } from './md-commander/hooks'; +export { initializeCommands, getCommands } from './md-commander/stores/commandsStore'; \ No newline at end of file diff --git a/src/components/md-commander/hooks/index.ts b/src/components/md-commander/hooks/index.ts index 0a6bd6a..60f6cfe 100644 --- a/src/components/md-commander/hooks/index.ts +++ b/src/components/md-commander/hooks/index.ts @@ -1,4 +1,4 @@ -export { useCommander, initializeCommands, defaultCommands } from "./useCommander"; +export { useCommander } from "./useCommander"; export type { UseCommanderReturn } from "./useCommander"; export { rollSimple, rollFormula } from "./useDiceRoller"; export type { DiceRollerResult } from "./useDiceRoller"; diff --git a/src/components/md-commander/hooks/useCommander.ts b/src/components/md-commander/hooks/useCommander.ts index 8036463..96d48f1 100644 --- a/src/components/md-commander/hooks/useCommander.ts +++ b/src/components/md-commander/hooks/useCommander.ts @@ -1,45 +1,14 @@ import { createSignal } from "solid-js"; import type { - MdCommanderCommand, - MdCommanderCommandMap, CommanderEntry, CompletionItem, TrackerItem, TrackerAttribute, TrackerCommand, TrackerViewMode, + MdCommanderCommandMap, } from "../types"; import { parseInput, getCompletions } from "./completions"; -import { setupHelpCommand, clearCommand, rollCommand, trackCommand, untrackCommand, listTrackCommand } from "../commands"; -import { - addTrackerItem as addTracker, - removeTrackerItem as removeTracker, - updateTrackerAttribute as updateTrackerAttr, - updateTrackerClasses as updateTrackerClassesStore, - moveTrackerItem as moveTracker, - removeTrackerClass as removeClassFromTracker, - getTrackerItems, - getTrackerHistory, - findTrackerIndex, -} from "../stores"; - -// ==================== 默认命令 ==================== - -export const defaultCommands: MdCommanderCommandMap = { - help: { command: "help" }, // 占位,稍后初始化 - clear: clearCommand, - roll: rollCommand, - track: trackCommand, - untrack: untrackCommand, - list: listTrackCommand, -}; - -// 初始化默认命令(包括 help) -export function initializeCommands(customCommands?: MdCommanderCommandMap): MdCommanderCommandMap { - const commands = { ...defaultCommands, ...customCommands }; - commands.help = setupHelpCommand(commands); - return commands; -} // ==================== Commander Hook ==================== @@ -58,30 +27,31 @@ export interface UseCommanderReturn { handleCommand: () => void; updateCompletions: () => void; acceptCompletion: () => void; - commands: MdCommanderCommandMap; + commands: () => MdCommanderCommandMap; + setCommands: (cmds: MdCommanderCommandMap) => void; historyIndex: () => number; setHistoryIndex: (v: number) => void; commandHistory: () => string[]; - navigateHistory: (direction: 'up' | 'down') => void; + navigateHistory: (direction: "up" | "down") => void; - // Tracker 相关 - 直接暴露 store API + // Tracker 相关 viewMode: () => TrackerViewMode; setViewMode: (mode: TrackerViewMode) => void; trackerItems: () => TrackerItem[]; trackerHistory: () => TrackerCommand[]; - addTrackerItem: typeof addTracker; - removeTrackerItem: typeof removeTracker; + addTrackerItem: (item: Omit) => TrackerItem; + removeTrackerItem: (emmet: string) => boolean; removeTrackerItemByIndex: (index: number) => void; - updateTrackerAttribute: typeof updateTrackerAttr; + updateTrackerAttribute: (emmet: string, attrName: string, attr: TrackerAttribute) => boolean; updateTrackerAttributeByIndex: (index: number, attrName: string, attr: TrackerAttribute) => void; updateTrackerClassesByIndex: (index: number, classes: string[]) => void; - moveTrackerItem: typeof moveTracker; - moveTrackerItemByIndex: (index: number, direction: 'up' | 'down') => void; - removeTrackerItemClass: typeof removeClassFromTracker; + moveTrackerItem: (emmet: string, direction: "up" | "down") => boolean; + moveTrackerItemByIndex: (index: number, direction: "up" | "down") => void; + removeTrackerItemClass: (emmet: string, className: string) => boolean; removeTrackerItemClassByIndex: (index: number, className: string) => void; } -export function useCommander(customCommands?: MdCommanderCommandMap): UseCommanderReturn { +export function useCommander(initialCommands?: MdCommanderCommandMap): UseCommanderReturn { const [inputValue, setInputValue] = createSignal(""); const [entries, setEntries] = createSignal([]); const [showCompletions, setShowCompletions] = createSignal(false); @@ -91,8 +61,7 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand const [commandHistory, setCommandHistory] = createSignal([]); const [historyIndex, setHistoryIndex] = createSignal(-1); const [viewMode, setViewMode] = createSignal("history"); - - const commands = initializeCommands(customCommands); + const [commands, setCommands] = createSignal(initialCommands || {}); // ==================== 命令执行 ==================== @@ -100,9 +69,10 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand const input = inputValue().trim(); if (!input) return; - const parsed = parseInput(input, commands); + const cmds = commands(); + const parsed = parseInput(input, cmds); const commandName = parsed.command; - const cmd = commands[commandName!]; + const cmd = cmds[commandName!]; let result: { message: string; type?: "success" | "error" | "warning" | "info"; isHtml?: boolean }; @@ -110,7 +80,7 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand result = { message: `未知命令:${commandName}`, type: "error" }; } else if (cmd.handler) { try { - result = cmd.handler({ params: parsed.params, options: parsed.options }, commands); + result = cmd.handler({ params: parsed.params, options: parsed.options }, cmds); } catch (e) { result = { message: `执行错误:${e instanceof Error ? e.message : String(e)}`, @@ -143,7 +113,7 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand // ==================== 自动补全 ==================== const updateCompletions = () => { - const comps = getCompletions(inputValue(), commands); + const comps = getCompletions(inputValue(), commands()); setCompletions(comps); setShowCompletions(comps.length > 0 && isFocused()); setSelectedCompletionState(0); @@ -151,8 +121,8 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand const setSelectedCompletion = (v: number | ((prev: number) => number)) => { const maxIdx = completions().length - 1; - if (typeof v === 'function') { - setSelectedCompletionState(prev => { + if (typeof v === "function") { + setSelectedCompletionState((prev) => { const next = v(prev); return Math.max(0, Math.min(next, maxIdx)); }); @@ -168,7 +138,7 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand if (!comp) return; const input = inputValue(); - const parsed = parseInput(input, commands); + const parsed = parseInput(input, commands()); let newValue: string; if (comp.type === "command") { @@ -179,7 +149,7 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand .join(" "); newValue = `${parsed.command || ""} ${existingOptions}${existingOptions ? " " : ""}${comp.insertText}`; } else if (comp.type === "value") { - const cmd = parsed.command ? commands[parsed.command] : null; + const cmd = parsed.command ? commands()[parsed.command] : null; const paramDefs = cmd?.parameters || []; const usedParams = Object.keys(parsed.params); const isTypingLastParam = paramDefs.length === usedParams.length && !input.endsWith(" "); @@ -204,12 +174,12 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand // ==================== 命令历史 ==================== - const navigateHistory = (direction: 'up' | 'down') => { + const navigateHistory = (direction: "up" | "down") => { const history = commandHistory(); if (history.length === 0) return; let newIndex = historyIndex(); - if (direction === 'up') { + if (direction === "up") { if (newIndex < history.length - 1) newIndex++; } else { if (newIndex > 0) { @@ -226,37 +196,47 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand }; // ==================== Tracker 操作 ==================== + const { + getTrackerItems, + getTrackerHistory, + addTrackerItem, + removeTrackerItem, + updateTrackerAttribute, + updateTrackerClasses, + moveTrackerItem, + removeTrackerClass, + } = require("../stores") as typeof import("../stores"); const getEmmetFromIndex = (index: number): string | null => { const items = getTrackerItems(); if (index < 0 || index >= items.length) return null; const item = items[index]; - return `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`; + return `${item.tag}${item.id ? "#" + item.id : ""}${item.classes.length > 0 ? "." + item.classes.join(".") : ""}`; }; const removeTrackerItemByIndex = (index: number) => { const emmet = getEmmetFromIndex(index); - if (emmet) removeTracker(emmet); + if (emmet) removeTrackerItem(emmet); }; const updateTrackerAttributeByIndex = (index: number, attrName: string, attr: TrackerAttribute) => { const emmet = getEmmetFromIndex(index); - if (emmet) updateTrackerAttr(emmet, attrName, attr); + if (emmet) updateTrackerAttribute(emmet, attrName, attr); }; const updateTrackerClassesByIndex = (index: number, classes: string[]) => { const emmet = getEmmetFromIndex(index); - if (emmet) updateTrackerClassesStore(emmet, classes); + if (emmet) updateTrackerClasses(emmet, classes); }; - const moveTrackerItemByIndex = (index: number, direction: 'up' | 'down') => { + const moveTrackerItemByIndex = (index: number, direction: "up" | "down") => { const emmet = getEmmetFromIndex(index); - if (emmet) moveTracker(emmet, direction); + if (emmet) moveTrackerItem(emmet, direction); }; const removeTrackerItemClassByIndex = (index: number, className: string) => { const emmet = getEmmetFromIndex(index); - if (emmet) removeClassFromTracker(emmet, className); + if (emmet) removeTrackerClass(emmet, className); }; return { @@ -275,6 +255,7 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand updateCompletions, acceptCompletion, commands, + setCommands, historyIndex, setHistoryIndex, commandHistory, @@ -283,15 +264,15 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand setViewMode, trackerItems: getTrackerItems, trackerHistory: getTrackerHistory, - addTrackerItem: addTracker, - removeTrackerItem: removeTracker, + addTrackerItem, + removeTrackerItem, removeTrackerItemByIndex, - updateTrackerAttribute: updateTrackerAttr, + updateTrackerAttribute, updateTrackerAttributeByIndex, updateTrackerClassesByIndex, - moveTrackerItem: moveTracker, + moveTrackerItem, moveTrackerItemByIndex, - removeTrackerItemClass: removeClassFromTracker, + removeTrackerItemClass, removeTrackerItemClassByIndex, }; } diff --git a/src/components/md-commander/index.tsx b/src/components/md-commander/index.tsx index cac1bd6..9b36175 100644 --- a/src/components/md-commander/index.tsx +++ b/src/components/md-commander/index.tsx @@ -1,13 +1,13 @@ import { customElement, noShadowDOM } from "solid-element"; -import { onMount, onCleanup, Show, createEffect } from "solid-js"; -import { useCommander, initializeCommands } from "./hooks"; +import { onMount, onCleanup, Show, createEffect, on } from "solid-js"; +import { useCommander } from "./hooks"; import { CommanderInput } from "./CommanderInput"; import { CommanderEntries } from "./CommanderEntries"; import { TrackerView } from "./TrackerView"; import { TabBar } from "./TabBar"; import type { MdCommanderProps } from "./types"; import { loadElementSrc, resolvePath } from "../utils/path"; -import { loadCommandTemplates } from "./utils/commandTemplates"; +import { initializeCommands, loadCommandTemplatesFromCSV } from "./stores/commandsStore"; customElement( "md-commander", @@ -15,13 +15,21 @@ customElement( (props, { element }) => { noShadowDOM(); const { articlePath, rawSrc } = loadElementSrc(element as any); - const commander = useCommander(props.commands); - createEffect(async () => { - if (!rawSrc) return; - const commands = initializeCommands(props.commands); - await loadCommandTemplates(commands, resolvePath(articlePath, rawSrc)); - }); + // 初始化命令并注册到 store + const commands = initializeCommands(props.commands); + const commander = useCommander(commands); + + // 加载 CSV 模板 + createEffect( + on(() => props.commandTemplates, async (csvPaths) => { + if (!csvPaths || !rawSrc) return; + await loadCommandTemplatesFromCSV(csvPaths, resolvePath, articlePath); + // 更新 commander 中的命令(从 store 获取) + const { getCommands } = await import("./stores/commandsStore"); + commander.setCommands(getCommands()); + }), + ); const handleKeyDown = (e: KeyboardEvent) => { if (commander.showCompletions() && commander.completions().length > 0) { diff --git a/src/components/md-commander/stores/commandsStore.ts b/src/components/md-commander/stores/commandsStore.ts new file mode 100644 index 0000000..a71c0ea --- /dev/null +++ b/src/components/md-commander/stores/commandsStore.ts @@ -0,0 +1,123 @@ +import { createStore } from "solid-js/store"; +import type { MdCommanderCommand, MdCommanderCommandMap } from "../types"; +import { setupHelpCommand, clearCommand, rollCommand, trackCommand, untrackCommand, listTrackCommand } from "../commands"; + +const defaultCommands: MdCommanderCommandMap = { + help: setupHelpCommand({}), + clear: clearCommand, + roll: rollCommand, + track: trackCommand, + untrack: untrackCommand, + list: listTrackCommand, +}; + +const [commandsStore, setCommandsStore] = createStore<{ + commands: MdCommanderCommandMap; + initialized: boolean; +}>({ + commands: { ...defaultCommands }, + initialized: false, +}); + +/** + * 初始化命令(包括 help 命令的动态更新) + */ +export function initializeCommands(customCommands?: MdCommanderCommandMap): MdCommanderCommandMap { + const commands = { ...defaultCommands, ...customCommands }; + // 更新 help 命令的命令列表 + commands.help = setupHelpCommand(commands); + return commands; +} + +/** + * 注册命令到 store + */ +export function registerCommands(customCommands?: MdCommanderCommandMap): void { + const commands = initializeCommands(customCommands); + setCommandsStore({ commands, initialized: true }); +} + +/** + * 从 store 获取命令 + */ +export function getCommands(): MdCommanderCommandMap { + return commandsStore.commands; +} + +/** + * 检查命令是否已初始化 + */ +export function isCommandsInitialized(): boolean { + return commandsStore.initialized; +} + +/** + * 获取单个命令 + */ +export function getCommand(name: string): MdCommanderCommand | undefined { + return commandsStore.commands[name]; +} + +/** + * 从 CSV 文件加载命令模板并更新命令定义 + */ +export async function loadCommandTemplatesFromCSV( + csvPaths: string | string[], + resolvePath: (base: string, path: string) => string, + articlePath: string +): Promise { + const paths = Array.isArray(csvPaths) ? csvPaths : [csvPaths]; + + for (const path of paths) { + try { + const { loadCSV } = await import("../../utils/csv-loader"); + const csv = await loadCSV(resolvePath(articlePath, path)); + + // 按命令分组模板 + const templatesByCommand = new Map(); + for (const row of csv) { + if (!row.command || !row.label || !row.insertedText) continue; + + if (!templatesByCommand.has(row.command)) { + templatesByCommand.set(row.command, []); + } + templatesByCommand.get(row.command)!.push(row); + } + + // 为每个命令添加模板 + setCommandsStore("commands", (prev) => { + const updated = { ...prev }; + for (const [commandName, rows] of templatesByCommand.entries()) { + const cmd = updated[commandName]; + if (!cmd || !cmd.parameters) continue; + + const templates = rows.map((row) => ({ + label: row.label, + description: row.description || "", + insertText: row.insertedText, + })); + + // 为每个参数添加模板 + updated[commandName] = { + ...cmd, + parameters: cmd.parameters.map((param) => ({ + ...param, + templates: param.templates ? [...param.templates, ...templates] : templates, + })), + }; + } + return updated; + }); + } catch (error) { + console.warn(`Error loading command templates from ${path}:`, error); + } + } +} + +interface CommandTemplateRow { + command: string; + parameter: string; + label: string; + description: string; + insertedText: string; +} diff --git a/src/components/md-commander/stores/index.ts b/src/components/md-commander/stores/index.ts index 5450ced..2f38b9e 100644 --- a/src/components/md-commander/stores/index.ts +++ b/src/components/md-commander/stores/index.ts @@ -12,3 +12,12 @@ export { findTrackerItem, } from "./trackerStore"; export type { TrackerStore } from "./trackerStore"; + +export { + initializeCommands, + registerCommands, + getCommands, + isCommandsInitialized, + getCommand, + loadCommandTemplatesFromCSV, +} from "./commandsStore"; diff --git a/src/components/md-commander/utils/commandTemplates.ts b/src/components/md-commander/utils/commandTemplates.ts deleted file mode 100644 index 2eabe48..0000000 --- a/src/components/md-commander/utils/commandTemplates.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { MdCommanderCommandMap, MdCommanderTemplate } from "../types"; -import { loadCSV } from "../../utils/csv-loader"; - -export interface CommandTemplateRow { - command: string; - parameter: string; - label: string; - description: string; - insertedText: string; -} - -/** - * 从 CSV 内容构建命令模板 - */ -function buildTemplatesFromRows(rows: CommandTemplateRow[]): MdCommanderTemplate[] { - return rows.map(row => ({ - label: row.label, - description: row.description || "", - insertText: row.insertedText, - })); -} - -/** - * 从 CSV 文件加载命令模板并更新命令定义 - */ -export async function loadCommandTemplates( - commands: MdCommanderCommandMap, - csvPaths: string | string[] -): Promise { - const paths = Array.isArray(csvPaths) ? csvPaths : [csvPaths]; - - for (const path of paths) { - try { - const csv = await loadCSV(path); - - // 按命令分组模板 - const templatesByCommand = new Map(); - for (const row of csv) { - if (!row.command || !row.label || !row.insertedText) continue; - - if (!templatesByCommand.has(row.command)) { - templatesByCommand.set(row.command, []); - } - templatesByCommand.get(row.command)!.push(row); - } - - // 为每个命令添加模板 - for (const [commandName, rows] of templatesByCommand.entries()) { - const cmd = commands[commandName]; - if (!cmd || !cmd.parameters) continue; - - const templates = buildTemplatesFromRows(rows); - - // 为每个参数添加模板 - for (const param of cmd.parameters) { - if (!param.templates) { - param.templates = []; - } - // 添加新模板 - param.templates.push(...templates); - } - } - } catch (error) { - console.warn(`Error loading command templates from ${path}:`, error); - } - } - - return commands; -}