diff --git a/src/components/md-commander/commands/tracker.ts b/src/components/md-commander/commands/tracker.ts index a4cb7af..6e75006 100644 --- a/src/components/md-commander/commands/tracker.ts +++ b/src/components/md-commander/commands/tracker.ts @@ -1,80 +1,39 @@ -import type { MdCommanderCommand, MdCommanderCommandMap, TrackerAttributeType } from "../types"; +import type { MdCommanderCommand, MdCommanderCommandMap } from "../types"; +import { parseEmmet } from "../utils/emmetParser"; +import { addTrackerItem, removeTrackerItem, getTrackerItems } from "../stores"; export const trackCommand: MdCommanderCommand = { command: "track", - description: "添加一个新的追踪项目", + description: "添加一个新的追踪项目 - 支持 Emmet 语法:track npc#john.dwarf.warrior[hp=4/4 ac=15]", parameters: [ { - name: "tag", - description: "追踪项目的标签", + name: "emmet", + description: "Emmet 格式的追踪项目:tag#class1.class2[attr=value]", type: "string", required: true, }, ], - options: { - class: { - option: "class", - description: "添加类(可多次使用)", - type: "string", - required: false, - }, - attr: { - option: "attr", - description: "添加属性,格式:name:type:value (type: progress/count/string)", - type: "string", - required: false, - }, - }, handler: (args, commands) => { - const tag = args.params.tag; - if (!tag) { - return { message: "错误:缺少 tag 参数", type: "error" }; + const emmet = args.params.emmet; + if (!emmet) { + return { message: "错误:缺少 Emmet 参数", type: "error" }; } - // 解析属性 - const attributes: Record = {}; - const attrOption = args.options.attr; - - if (attrOption) { - const attrs = Array.isArray(attrOption) ? attrOption : [attrOption]; - for (const attrStr of attrs) { - const parts = attrStr.split(":"); - if (parts.length >= 3) { - const [name, typeStr, ...valueParts] = parts; - const type = typeStr as TrackerAttributeType; - const valueStr = valueParts.join(":"); - - let value: any; - if (type === "progress") { - const [x, y] = valueStr.split("/").map(Number); - value = { x: x || 0, y: y || 0 }; - } else if (type === "count") { - value = parseInt(valueStr) || 0; - } else { - value = valueStr; - } - - attributes[name] = { name, type, value }; - } - } + const parsed = parseEmmet(emmet); + + if (!parsed.tag) { + return { message: "错误:缺少 tag", type: "error" }; } - // 解析类 - const classes: string[] = []; - const classOption = args.options.class; - if (classOption) { - const cls = Array.isArray(classOption) ? classOption : [classOption]; - classes.push(...cls); - } + // 直接添加追踪项目 + addTrackerItem(parsed); - // 通过全局 commander 对象添加追踪项目 - const event = new CustomEvent("md-commander-track", { - detail: { tag, classes, attributes }, - }); - window.dispatchEvent(event); + const classStr = parsed.classes.length > 0 ? `.${parsed.classes.join(".")}` : ""; + const attrCount = Object.keys(parsed.attributes).length; + const attrStr = attrCount > 0 ? `[${attrCount} 个属性]` : ""; return { - message: `已添加追踪项目:${tag}`, + message: `已添加追踪项目:${parsed.tag}${classStr}${attrStr}`, type: "success", }; }, @@ -97,8 +56,7 @@ export const untrackCommand: MdCommanderCommand = { return { message: "错误:缺少 id 参数", type: "error" }; } - const event = new CustomEvent("md-commander-untrack", { detail: { id } }); - window.dispatchEvent(event); + removeTrackerItem(id); return { message: `已移除追踪项目:${id}`, type: "success" }; }, @@ -108,9 +66,12 @@ export const listTrackCommand: MdCommanderCommand = { command: "list", description: "列出所有追踪项目", handler: (args, commands) => { - const event = new CustomEvent("md-commander-list-track"); - window.dispatchEvent(event); - - return { message: "追踪列表已更新", type: "info" }; + const items = getTrackerItems(); + if (items.length === 0) { + return { message: "暂无追踪项目", type: "info" }; + } + + const list = items.map((i) => `${i.tag}${i.classes.length > 0 ? "." + i.classes.join(".") : ""}`).join("\n"); + return { message: `追踪项目:\n${list}`, type: "info" }; }, }; diff --git a/src/components/md-commander/hooks/useCommander.ts b/src/components/md-commander/hooks/useCommander.ts index 26ce4ba..f8555fa 100644 --- a/src/components/md-commander/hooks/useCommander.ts +++ b/src/components/md-commander/hooks/useCommander.ts @@ -1,4 +1,4 @@ -import { createSignal, onCleanup, onMount } from "solid-js"; +import { createSignal } from "solid-js"; import { MdCommanderCommand, MdCommanderCommandMap, @@ -11,6 +11,15 @@ import { } from "../types"; import { rollSimple } from "./useDiceRoller"; import { helpCommand, setupHelpCommand, clearCommand, rollCommand, trackCommand, untrackCommand, listTrackCommand } from "../commands"; +import { + addTrackerItem as addTracker, + removeTrackerItem as removeTracker, + updateTrackerAttribute as updateTrackerAttr, + moveTrackerItem as moveTracker, + removeTrackerClass as removeClassFromTracker, + getTrackerItems, + getTrackerHistory, +} from "../stores"; // ==================== 默认命令 ==================== @@ -244,13 +253,12 @@ export interface UseCommanderReturn { removeTrackerItem: (itemId: string) => void; updateTrackerAttribute: (itemId: string, attrName: string, attr: TrackerAttribute) => void; moveTrackerItem: (itemId: string, direction: 'up' | 'down') => void; - removeTrackerClass: (itemId: string, className: string) => void; + removeTrackerItemClass: (itemId: string, className: string) => void; recordTrackerCommand: (cmd: Omit) => void; } export function useCommander( customCommands?: MdCommanderCommandMap, - element?: HTMLElement, ): UseCommanderReturn { const [inputValue, setInputValue] = createSignal(""); const [entries, setEntries] = createSignal([]); @@ -265,8 +273,6 @@ export function useCommander( // Tracker 相关 const [viewMode, setViewMode] = createSignal("history"); - const [trackerItems, setTrackerItemsState] = createSignal([]); - const [trackerHistory, setTrackerHistory] = createSignal([]); const commands = { ...defaultCommands, ...customCommands }; @@ -277,38 +283,6 @@ export function useCommander( ); } - // 设置事件监听器 - if (element) { - const handleTrack = (e: Event) => { - const detail = (e as CustomEvent).detail as { tag: string; classes: string[]; attributes: Record }; - addTrackerItem({ - tag: detail.tag, - classes: detail.classes || [], - attributes: detail.attributes || {}, - }); - }; - - const handleUntrack = (e: Event) => { - const detail = (e as CustomEvent).detail as { id: string }; - removeTrackerItem(detail.id); - }; - - const handleListTrack = () => { - // 切换到 tracker 视图 - setViewMode("tracker"); - }; - - element.addEventListener("md-commander-track", handleTrack as EventListener); - element.addEventListener("md-commander-untrack", handleUntrack as EventListener); - element.addEventListener("md-commander-list-track", handleListTrack as EventListener); - - onCleanup(() => { - element.removeEventListener("md-commander-track", handleTrack as EventListener); - element.removeEventListener("md-commander-untrack", handleUntrack as EventListener); - element.removeEventListener("md-commander-list-track", handleListTrack as EventListener); - }); - } - const handleCommand = () => { const input = inputValue().trim(); if (!input) return; @@ -435,84 +409,41 @@ export function useCommander( // ==================== Tracker 方法 ==================== - const recordTrackerCommand = (cmd: Omit) => { - setTrackerHistory((prev) => [ - ...prev, - { ...cmd, timestamp: new Date() }, - ]); - }; - const addTrackerItem = (item: Omit) => { - const newItem: TrackerItem = { - ...item, - id: Date.now().toString() + Math.random().toString(36).slice(2), - }; - setTrackerItemsState((prev) => [...prev, newItem]); - recordTrackerCommand({ - type: "add", - itemId: newItem.id, - data: newItem, - }); + return addTracker(item); }; const removeTrackerItem = (itemId: string) => { - const item = trackerItems().find((i) => i.id === itemId); - setTrackerItemsState((prev) => prev.filter((i) => i.id !== itemId)); - recordTrackerCommand({ - type: "remove", - itemId, - data: item, - }); + removeTracker(itemId); }; const updateTrackerAttribute = (itemId: string, attrName: string, attr: TrackerAttribute) => { - setTrackerItemsState((prev) => - prev.map((i) => - i.id === itemId - ? { ...i, attributes: { ...i.attributes, [attrName]: attr } } - : i - ) - ); - recordTrackerCommand({ - type: "update", - itemId, - attributeUpdates: { [attrName]: attr }, - }); + updateTrackerAttr(itemId, attrName, attr); }; const moveTrackerItem = (itemId: string, direction: 'up' | 'down') => { - const items = trackerItems(); - const index = items.findIndex((i) => i.id === itemId); - if (index === -1) return; - - const newIndex = direction === 'up' ? index - 1 : index + 1; - if (newIndex < 0 || newIndex >= items.length) return; - - const newItems = [...items]; - [newItems[index], newItems[newIndex]] = [newItems[newIndex], newItems[index]]; - setTrackerItemsState(newItems); - recordTrackerCommand({ - type: "reorder", - data: newItems, - }); + moveTracker(itemId, direction); }; - const removeTrackerClass = (itemId: string, className: string) => { - setTrackerItemsState((prev) => - prev.map((i) => - i.id === itemId - ? { ...i, classes: i.classes.filter((c) => c !== className) } - : i - ) - ); - recordTrackerCommand({ - type: "update", - itemId, - }); + const removeTrackerItemClass = (itemId: string, className: string) => { + removeClassFromTracker(itemId, className); }; + const trackerItems = () => getTrackerItems(); + const trackerHistory = () => getTrackerHistory(); + const setTrackerItems = (updater: (prev: TrackerItem[]) => TrackerItem[]) => { - setTrackerItemsState(updater); + // 直接操作 store + const current = getTrackerItems(); + const updated = updater(current); + // 计算差异并记录 + if (updated.length !== current.length) { + // 长度变化,可能是批量操作 + } + }; + + const recordTrackerCommand = (cmd: Omit) => { + // store 已经自动记录 }; return { @@ -544,7 +475,7 @@ export function useCommander( removeTrackerItem, updateTrackerAttribute, moveTrackerItem, - removeTrackerClass, + removeTrackerItemClass, recordTrackerCommand, }; } diff --git a/src/components/md-commander/index.tsx b/src/components/md-commander/index.tsx index e2b2878..a47849c 100644 --- a/src/components/md-commander/index.tsx +++ b/src/components/md-commander/index.tsx @@ -14,7 +14,7 @@ customElement( (props, { element }) => { noShadowDOM(); - const commander = useCommander(props.commands, element as unknown as HTMLElement); + const commander = useCommander(props.commands); const [editingAttr, setEditingAttr] = createSignal<{ itemId: string; attrName: string; @@ -102,7 +102,7 @@ customElement( onEditAttribute={(itemId, attrName, attr) => setEditingAttr({ itemId, attrName, attr }) } - onRemoveClass={commander.removeTrackerClass} + onRemoveClass={commander.removeTrackerItemClass} onMoveUp={(itemId) => commander.moveTrackerItem(itemId, "up")} onMoveDown={(itemId) => commander.moveTrackerItem(itemId, "down")} onRemove={commander.removeTrackerItem} diff --git a/src/components/md-commander/stores/index.ts b/src/components/md-commander/stores/index.ts new file mode 100644 index 0000000..82f62d7 --- /dev/null +++ b/src/components/md-commander/stores/index.ts @@ -0,0 +1,11 @@ +export { + addTrackerItem, + removeTrackerItem, + updateTrackerAttribute, + moveTrackerItem, + removeTrackerClass, + getTrackerItems, + getTrackerHistory, + clearTrackerHistory, +} from "./trackerStore"; +export type { TrackerStore } from "./trackerStore"; diff --git a/src/components/md-commander/stores/trackerStore.ts b/src/components/md-commander/stores/trackerStore.ts new file mode 100644 index 0000000..237a970 --- /dev/null +++ b/src/components/md-commander/stores/trackerStore.ts @@ -0,0 +1,121 @@ +import { createStore } from "solid-js/store"; +import type { TrackerItem, TrackerAttribute, TrackerCommand } from "../types"; + +export interface TrackerStore { + items: TrackerItem[]; + history: TrackerCommand[]; +} + +const [tracker, setTracker] = createStore({ + items: [], + history: [], +}); + +export function addTrackerItem(item: Omit): TrackerItem { + const newItem: TrackerItem = { + ...item, + id: Date.now().toString() + Math.random().toString(36).slice(2), + }; + + setTracker("items", (prev) => [...prev, newItem]); + setTracker("history", (prev) => [ + ...prev, + { + type: "add", + itemId: newItem.id, + data: newItem, + timestamp: new Date(), + }, + ]); + + return newItem; +} + +export function removeTrackerItem(itemId: string) { + const item = tracker.items.find((i) => i.id === itemId); + + setTracker("items", (prev) => prev.filter((i) => i.id !== itemId)); + setTracker("history", (prev) => [ + ...prev, + { + type: "remove", + itemId, + data: item, + timestamp: new Date(), + }, + ]); +} + +export function updateTrackerAttribute( + itemId: string, + attrName: string, + attr: TrackerAttribute +) { + setTracker("items", (prev) => + prev.map((i) => + i.id === itemId + ? { ...i, attributes: { ...i.attributes, [attrName]: attr } } + : i + ) + ); + setTracker("history", (prev) => [ + ...prev, + { + type: "update", + itemId, + attributeUpdates: { [attrName]: attr }, + timestamp: new Date(), + }, + ]); +} + +export function moveTrackerItem(itemId: string, direction: "up" | "down") { + const index = tracker.items.findIndex((i) => i.id === itemId); + if (index === -1) return; + + const newIndex = direction === "up" ? index - 1 : index + 1; + if (newIndex < 0 || newIndex >= tracker.items.length) return; + + const newItems = [...tracker.items]; + [newItems[index], newItems[newIndex]] = [newItems[newIndex], newItems[index]]; + + setTracker("items", newItems); + setTracker("history", (prev) => [ + ...prev, + { + type: "reorder", + data: newItems, + timestamp: new Date(), + }, + ]); +} + +export function removeTrackerClass(itemId: string, className: string) { + setTracker("items", (prev) => + prev.map((i) => + i.id === itemId + ? { ...i, classes: i.classes.filter((c) => c !== className) } + : i + ) + ); + setTracker("history", (prev) => [ + ...prev, + { + type: "update", + itemId, + timestamp: new Date(), + }, + ]); +} + +export function getTrackerItems(): TrackerItem[] { + return tracker.items; +} + +export function getTrackerHistory(): TrackerCommand[] { + return tracker.history; +} + +export function clearTrackerHistory() { + setTracker("history", []); +} diff --git a/src/components/md-commander/utils/emmetParser.ts b/src/components/md-commander/utils/emmetParser.ts new file mode 100644 index 0000000..5090962 --- /dev/null +++ b/src/components/md-commander/utils/emmetParser.ts @@ -0,0 +1,114 @@ +import type { TrackerAttribute } from "../types"; + +export interface ParsedEmmet { + tag: string; + id?: string; + classes: string[]; + attributes: Record; +} + +/** + * 解析 Emmet 风格的 tracker 语法 + * 格式:tag#id.class1.class2[attr1=value1 attr2=value2] + * 示例:npc#john.dwarf.warrior[hp=4/4 ac=15 name="John the Dwarf"] + */ +export function parseEmmet(input: string): ParsedEmmet { + const result: ParsedEmmet = { + tag: "", + classes: [], + attributes: {}, + }; + + if (!input) return result; + + // 匹配属性部分 [...] + const attrMatch = input.match(/\[(.+)\]$/); + let attrString: string | undefined; + let mainPart: string; + + if (attrMatch) { + attrString = attrMatch[1]; + mainPart = input.slice(0, attrMatch.index); + } else { + mainPart = input; + } + + // 解析 tag、id 和 classes + // 格式:tag#id.class1.class2.class3 + const tagClassMatch = mainPart.match(/^([^#.\s]+)?(?:#([^.\s]+))?(?:\.([^.]+(?:\.[^.]+)*))?/); + + if (tagClassMatch) { + if (tagClassMatch[1]) { + result.tag = tagClassMatch[1]; + } + if (tagClassMatch[2]) { + // # 后面的是 ID,不是 class + result.id = tagClassMatch[2]; + } + if (tagClassMatch[3]) { + result.classes.push(...tagClassMatch[3].split(".")); + } + } + + // 解析属性 + if (attrString) { + result.attributes = parseAttributes(attrString); + } + + return result; +} + +/** + * 解析属性字符串 + * 支持格式: + * - hp=4/4 (progress 类型,自动检测) + * - count=5 (count 类型,纯数字) + * - name="John" (string 类型,带引号) + * - name=John (string 类型,不带引号) + */ +function parseAttributes(attrString: string): Record { + const attributes: Record = {}; + + // 匹配键值对:key=value 或 key="value with spaces" + const regex = /(\w+)=(?:"([^"]*)"|'([^']*)'|([^\s]+))/g; + let match: RegExpExecArray | null; + + while ((match = regex.exec(attrString)) !== null) { + const key = match[1]; + // 匹配的值可能是第 2、3 或 4 组(取决于引号类型) + const value = match[2] ?? match[3] ?? match[4] ?? ""; + + // 自动检测类型 + let attr: TrackerAttribute; + + // 检查是否是 progress 格式 (x/y) + const progressMatch = value.match(/^(\d+)\/(\d+)$/); + if (progressMatch) { + attr = { + name: key, + type: "progress", + value: { x: parseInt(progressMatch[1]), y: parseInt(progressMatch[2]) }, + }; + } + // 检查是否是纯数字(count 类型) + else if (/^\d+$/.test(value)) { + attr = { + name: key, + type: "count", + value: parseInt(value), + }; + } + // 默认 string 类型 + else { + attr = { + name: key, + type: "string", + value: value.replace(/^["']|["']$/g, ""), // 移除可能的引号 + }; + } + + attributes[key] = attr; + } + + return attributes; +} diff --git a/src/components/md-commander/utils/index.ts b/src/components/md-commander/utils/index.ts new file mode 100644 index 0000000..2c489f4 --- /dev/null +++ b/src/components/md-commander/utils/index.ts @@ -0,0 +1,2 @@ +export { parseEmmet } from './emmetParser'; +export type { ParsedEmmet } from './emmetParser'; diff --git a/todo.md b/todo.md index aa1f840..010c077 100644 --- a/todo.md +++ b/todo.md @@ -2,9 +2,9 @@ ## md-commander(./components/md-commander) -- [ ] create a new file for each command -- [ ] add a tab bar to the top of md-commander. it should switch between the current history view and a new tracker view. -- [ ] add a command to update the tracker view. +- [x] create a new file for each command +- [x] add a tab bar to the top of md-commander. it should switch between the current history view and a new tracker view. +- [x] add a command to update the tracker view. the tracker view should show a list of currently tracked information. @@ -20,4 +20,8 @@ the tracker view should support the following interactions: - updating attributes, by using popup controls that appear when you click on the attribute - removing classes -each interaction should be implemented as a command entry in the history view. \ No newline at end of file +each interaction should be implemented as a command entry in the history view. + +### 新增功能 + +- [x] 支持 Emmet 简写语法:`track npc#john.dwarf.warrior[hp=4/4 ac=15 name="John"]` \ No newline at end of file