dsh-muv-table 0.2.9 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  // MUV Initvar parser: converts YAML-like indented initvar text to nested objects.
2
- // Format: 2-space indentation, `键: 值` pairs. Supports nested objects and inline
3
- // lists (comma-separated values wrapped in [ ]).
2
+ // Format: 2-space indentation, `键: 值` pairs. Supports nested objects, `- item`
3
+ // lists, and inline lists (comma-separated values wrapped in [ ]).
4
4
 
5
5
  /**
6
6
  * Parse an initvar text block into a nested object.
@@ -8,9 +8,12 @@
8
8
  * @returns {object} Parsed nested object
9
9
  */
10
10
  export function parseInitvar(text) {
11
- const lines = text.split(/\r?\n/)
11
+ const lines = String(text ?? '').split(/\r?\n/)
12
12
  const root = {}
13
- const stack = [{ obj: root, indent: -1 }]
13
+ // Each frame remembers the key that opened it and the object that holds it, so
14
+ // a frame created as an assumed-empty object can still be turned into an array
15
+ // once its contents turn out to be `- item` lines.
16
+ const stack = [{ obj: root, indent: -1, key: null, parent: null, list: false }]
14
17
 
15
18
  for (const rawLine of lines) {
16
19
  const line = rawLine.trimEnd()
@@ -19,16 +22,55 @@ export function parseInitvar(text) {
19
22
  const indent = line.length - line.trimStart().length
20
23
  const trimmed = line.trim()
21
24
 
25
+ // A `- item` line is a list entry, not a `键: 值` pair. It used to be dropped
26
+ // outright (`colonIdx === -1` → continue), which silently emptied every list
27
+ // a card declares the way 苍玄界 does:
28
+ //
29
+ // 最近互动记录:
30
+ // - 因为想吃灵鹤被{{user}}抓包,目前心虚加不知所措。
31
+ //
32
+ // The key before it looks like an empty value, so the entry belongs to the
33
+ // innermost *pending* key: that key was optimistically created as `{}` and is
34
+ // now revealed to be an array. Checked before the stack pop because a list is
35
+ // allowed to sit at the same indent as its own key.
36
+ const itemMatch = /^-(?:\s+(.*))?$/.exec(trimmed)
37
+ if (itemMatch) {
38
+ const frame = stack[stack.length - 1]
39
+ if (frame.key !== null && !frame.list && Object.keys(frame.obj).length === 0) {
40
+ const arr = []
41
+ frame.parent[frame.key] = arr
42
+ frame.obj = arr
43
+ frame.list = true
44
+ }
45
+ // No pending key above us means a stray item with nowhere to go; dropping
46
+ // it is the same behaviour this line always had.
47
+ if (frame.list) frame.obj.push(parseValue((itemMatch[1] ?? '').trim()))
48
+ continue
49
+ }
50
+
22
51
  // Pop stack until we find the right parent
23
52
  while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
24
53
  stack.pop()
25
54
  }
26
55
 
27
- const parent = stack[stack.length - 1].obj
56
+ const parentFrame = stack[stack.length - 1]
57
+ const parent = parentFrame.obj
28
58
  const colonIdx = trimmed.indexOf(':')
29
59
 
30
60
  if (colonIdx === -1) {
31
- // Value-only line (continuation or list item)
61
+ // Value-only line (continuation) — not representable in this format, and
62
+ // dropped exactly as before. Inside a list there are no keys at all, so the
63
+ // raw line is kept as an item rather than vanishing.
64
+ if (parentFrame.list) parent.push(trimmed)
65
+ continue
66
+ }
67
+
68
+ // A `键: 值` line under a list is not a key either (`- a` followed by an
69
+ // indented `x: 1` is a list of objects, which this format cannot express).
70
+ // Keeping the text as an item preserves the content; writing it as a property
71
+ // of an array would be silently lost by JSON.stringify.
72
+ if (parentFrame.list) {
73
+ parent.push(trimmed)
32
74
  continue
33
75
  }
34
76
 
@@ -36,10 +78,10 @@ export function parseInitvar(text) {
36
78
  const value = trimmed.slice(colonIdx + 1).trim()
37
79
 
38
80
  if (value === '') {
39
- // Nested object start
81
+ // Nested object start — may still turn out to be a list (see above).
40
82
  const newObj = {}
41
83
  parent[key] = newObj
42
- stack.push({ obj: newObj, indent })
84
+ stack.push({ obj: newObj, indent, key, parent, list: false })
43
85
  } else {
44
86
  // Leaf value
45
87
  parent[key] = parseValue(value)
@@ -52,12 +94,14 @@ export function parseInitvar(text) {
52
94
  /**
53
95
  * Parse a leaf value string to appropriate type.
54
96
  * @param {string} val
55
- * @returns {string|number|boolean}
97
+ * @returns {string|number|boolean|Array}
56
98
  */
57
99
  function parseValue(val) {
58
100
  // Empty string
59
101
  if (val === '""' || val === "''") return ''
60
102
  if (val === '{}') return '{}'
103
+ // Empty list — serialized that way so it does not come back as `{}`.
104
+ if (val === '[]') return []
61
105
 
62
106
  // Number
63
107
  if (/^-?\d+(\.\d+)?$/.test(val)) {
@@ -82,7 +126,16 @@ export function serializeInitvar(obj, depth = 0) {
82
126
  let result = ''
83
127
 
84
128
  for (const [key, value] of Object.entries(obj)) {
85
- if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
129
+ if (Array.isArray(value)) {
130
+ // Lists are written the way the cards write them (`键:` then ` - item`), so
131
+ // a parsed list survives a round trip instead of collapsing into "a,b".
132
+ if (value.length === 0) {
133
+ result += `${indent}${key}: []\n`
134
+ } else {
135
+ result += `${indent}${key}:\n`
136
+ for (const item of value) result += `${indent} - ${formatValue(item)}\n`
137
+ }
138
+ } else if (value !== null && typeof value === 'object') {
86
139
  result += `${indent}${key}:\n`
87
140
  result += serializeInitvar(value, depth + 1)
88
141
  } else {
@@ -97,5 +150,10 @@ function formatValue(val) {
97
150
  if (val === null || val === undefined) return ''
98
151
  if (typeof val === 'boolean') return val ? 'true' : 'false'
99
152
  if (typeof val === 'number') return String(val)
153
+ // An empty string must stay quoted. Written bare (`键: `) the line looks like
154
+ // the start of a nested object, so 苍玄界's `在场角色: ""` came back as `{}`
155
+ // the next time the block was read — a silent type flip. The cards quote it
156
+ // themselves; this just keeps that convention on the way out.
157
+ if (val === '') return '""'
100
158
  return String(val)
101
159
  }
package/lib/muv-parser.js CHANGED
@@ -17,6 +17,142 @@ import { parseInitvar } from './initvar-parser.js'
17
17
  // as well park it in description/scenario, so all three are scanned.
18
18
  const VARIABLE_TEXT_FIELDS = ['first_mes', 'description', 'scenario']
19
19
 
20
+ /** Arrays only — a malformed card may carry an object where a list belongs. */
21
+ function asArray(value) {
22
+ return Array.isArray(value) ? value : []
23
+ }
24
+
25
+ /**
26
+ * Every text blob of a card that might hold `<initvar>` / `<VariableInsert>`.
27
+ *
28
+ * Besides the obvious greeting fields, two more sources carry these blocks in
29
+ * the field and used to be skipped entirely:
30
+ *
31
+ * - world book entries (`character_book.entries[].content`) — `[initvar]变量初始化勿开`
32
+ * is a world-book entry, not a greeting, in the cards that use this shape;
33
+ * - `tavern_helper` scripts, whose source often contains the same block.
34
+ *
35
+ * The list is ordered by preference and grouped, so a caller can ask for "the
36
+ * greeting-level sources first" without hardcoding field names again.
37
+ * @param {object} data - the card's `data` object
38
+ * @returns {Array<{group: string, where: string, text: string}>}
39
+ */
40
+ function variableTextCandidates(data) {
41
+ const out = []
42
+ if (!data || typeof data !== 'object') return out
43
+
44
+ const push = (group, where, text) => {
45
+ if (typeof text === 'string') out.push({ group, where, text })
46
+ }
47
+
48
+ for (const field of VARIABLE_TEXT_FIELDS) push('field', field, data[field])
49
+ for (const greeting of asArray(data.alternate_greetings)) {
50
+ push('greeting', 'alternate_greetings', greeting)
51
+ }
52
+ for (const entry of asArray(data.character_book?.entries)) {
53
+ push('worldbook', 'character_book:' + String(entry?.comment || ''), entry?.content)
54
+ }
55
+ for (const script of asArray(data.extensions?.tavern_helper?.scripts)) {
56
+ push('script', 'tavern_helper:' + String(script?.name || ''), script?.content)
57
+ }
58
+ return out
59
+ }
60
+
61
+ /** All texts belonging to the given candidate groups, in candidate order. */
62
+ function textsInGroups(candidates, ...groups) {
63
+ return candidates.filter(c => groups.includes(c.group)).map(c => c.text)
64
+ }
65
+
66
+ /**
67
+ * World-book entries that hold a **bare** initvar block — no `<initvar>` tag at all.
68
+ *
69
+ * `异世界农场` keeps its entire variable tree in an entry whose comment is
70
+ * `[initvar]变量初始化勿开` and whose content is plain YAML:
71
+ *
72
+ * 时间:
73
+ * 日期: '05-20'
74
+ * 星期: 周日
75
+ * …
76
+ *
77
+ * Because there is no tag, the tag-based scan can never reach it and the card
78
+ * reported `schemas: 0` even though the whole tree was sitting in the world book.
79
+ *
80
+ * The prefix must be exactly `[initvar]`. The sibling conventions are **documents,
81
+ * not data** and reading them as YAML would build a junk table:
82
+ *
83
+ * - `[mvu_update]变量输出格式` / `[mvu_update]变量更新规则` — how to emit updates
84
+ * (苍玄界 and 异世界农场 both carry these);
85
+ * - `[mvu_plot]…` — illustration prompts.
86
+ *
87
+ * @param {object} data - the card's `data` object
88
+ * @returns {Array<{comment: string, content: string}>}
89
+ */
90
+ function worldbookInitvarEntries(data) {
91
+ const out = []
92
+ for (const entry of asArray(data?.character_book?.entries)) {
93
+ if (!/^\s*\[initvar\]/i.test(String(entry?.comment || ''))) continue
94
+ if (typeof entry?.content === 'string') out.push({ comment: String(entry.comment), content: entry.content })
95
+ }
96
+ return out
97
+ }
98
+
99
+ /**
100
+ * Does this text read as a YAML **mapping of variables**, rather than prose or a
101
+ * document that happens to mention keys?
102
+ *
103
+ * `parseInitvar` never fails — it drops what it cannot use and returns whatever is
104
+ * left, so "it parsed" is not evidence. This is the actual shape test used to decide
105
+ * whether a `[initvar]`-marked entry is data:
106
+ *
107
+ * - blanks and `# comment` lines are ignored;
108
+ * - `键:` / `键: 值` lines count as keys;
109
+ * - `- item` lines count as list content belonging to the nearest key;
110
+ * - **any other line means this is not a variable mapping** → reject.
111
+ *
112
+ * That last rule is what keeps a prose document out: Chinese prose uses the full-width
113
+ * `:`, so a sentence like `变量初始化格式说明:…` produces no key line at all, and a
114
+ * paragraph next to a stray `键:` line is still rejected (one foreign line is enough).
115
+ * @param {string} text
116
+ * @returns {boolean}
117
+ */
118
+ function looksLikeInitvarMapping(text) {
119
+ let keys = 0
120
+ for (const rawLine of String(text ?? '').split(/\r?\n/)) {
121
+ const line = rawLine.trim()
122
+ if (!line || line.startsWith('#')) continue
123
+ if (/^-(?:\s|$)/.test(line)) continue
124
+ if (/^[^:\s#][^:]*:(\s|$)/.test(line)) { keys++; continue }
125
+ return false
126
+ }
127
+ return keys > 0
128
+ }
129
+
130
+ /** The first `<initvar>` block in a list of texts, or null. */
131
+ function firstInitvarBlock(texts) {
132
+ for (const text of texts) {
133
+ const match = text.match(/<initvar>([\s\S]*?)<\/initvar>/i)
134
+ if (match) return { raw: match[1].trim(), parsed: parseInitvar(match[1]) }
135
+ }
136
+ return null
137
+ }
138
+
139
+ /**
140
+ * A card's variable data, whichever spec shape it uses.
141
+ *
142
+ * Spec-v2 cards wrap every field in `data`; spec-v1 (flat) cards put the same
143
+ * fields at the top level. Reading only `data` made a flat card — one
144
+ * `findJsonMatch` happily found by name — parse as `{name:'Unnamed'}` with 0
145
+ * fields, i.e. the library file was located and then thrown away.
146
+ * @param {object} cardJson
147
+ * @returns {object}
148
+ */
149
+ function cardData(cardJson) {
150
+ if (!cardJson || typeof cardJson !== 'object') return {}
151
+ const inner = cardJson.data
152
+ if (inner && typeof inner === 'object' && !Array.isArray(inner)) return inner
153
+ return cardJson
154
+ }
155
+
20
156
  /**
21
157
  * Drop a collection's `$template` placeholder.
22
158
  *
@@ -46,15 +182,7 @@ export function stripPlaceholderEntries(data) {
46
182
  export function extractVariableInsert(data) {
47
183
  if (!data || typeof data !== 'object') return null
48
184
 
49
- const candidates = []
50
- for (const field of VARIABLE_TEXT_FIELDS) {
51
- if (typeof data[field] === 'string') candidates.push(data[field])
52
- }
53
- for (const greeting of data.alternate_greetings || []) {
54
- if (typeof greeting === 'string') candidates.push(greeting)
55
- }
56
-
57
- for (const text of candidates) {
185
+ for (const { text } of variableTextCandidates(data)) {
58
186
  const match = text.match(/<VariableInsert>([\s\S]*?)<\/VariableInsert>/i)
59
187
  if (!match) continue
60
188
  const raw = match[1].trim()
@@ -65,6 +193,9 @@ export function extractVariableInsert(data) {
65
193
  }
66
194
  } catch (_) {
67
195
  // Not JSON — keep looking; another block might be the real one.
196
+ // World-book entries and helper scripts routinely *quote* the tags in
197
+ // prose (「必须使用 `<VariableInsert>`」) or inside a fenced code block, and
198
+ // those examples are never valid JSON, so this is the common case there.
68
199
  }
69
200
  }
70
201
  return null
@@ -73,11 +204,15 @@ export function extractVariableInsert(data) {
73
204
  /**
74
205
  * Parse a MUV character card JSON and extract schema + variable data.
75
206
  * @param {object} cardJson - The full character card JSON
76
- * @returns {{ name: string, schemas: object[], initvarData: object, zodSource: string }}
207
+ * @returns {{ name: string, schemas: object[], initvarData: object, zodSource: string,
208
+ * schemaSource: 'initvar'|'variable-insert'|'worldbook-initvar'|'none' }}
209
+ * `schemaSource` names the source the variable tree was read from ('none' when the
210
+ * card carries none), so a reader can tell a real tree from the heuristic fallback.
77
211
  */
78
212
  export function parseMuvCard(cardJson) {
79
- const data = cardJson?.data || {}
80
- const name = data.name || cardJson?.name || 'Unnamed'
213
+ const data = cardData(cardJson)
214
+ const name = data.name || cardJson?.data?.name || cardJson?.name || 'Unnamed'
215
+ const candidates = variableTextCandidates(data)
81
216
  const tavernHelper = data.extensions?.tavern_helper
82
217
 
83
218
  // Extract Zod schema source (for reference)
@@ -87,18 +222,17 @@ export function parseMuvCard(cardJson) {
87
222
  // (`z.object({...})` / `registerMvuSchema`)其实是 MUV 最主流的 schema 载体,
88
223
  // 读不到就等于整张卡的变量表建不出来。
89
224
  // 现在按「名字含 zod」或「内容像 Zod schema」两条线索找,兼容各种命名。
90
- const scripts = tavernHelper?.scripts || []
225
+ // `scripts` 不一定是数组(畸形卡),先归一化再找,否则这里会先抛 TypeError。
226
+ const scripts = asArray(tavernHelper?.scripts)
91
227
  const zodScript = scripts.find(s => /zod/i.test(String(s?.name || '')))
92
228
  || scripts.find(s => /z\.object\s*\(|registerMvuSchema|Mvu\.|z\.string\s*\(|prefault\s*\(/.test(String(s?.content || '')))
93
229
  const zodSource = zodScript?.content || ''
94
230
 
95
231
  // Extract initvar blocks from alternate_greetings
96
- const greetings = data.alternate_greetings || []
97
232
  const initvarBlocks = []
98
233
 
99
- for (const greeting of greetings) {
100
- if (typeof greeting !== 'string') continue
101
- const match = greeting.match(/<initvar>([\s\S]*?)<\/initvar>/i)
234
+ for (const text of textsInGroups(candidates, 'greeting')) {
235
+ const match = text.match(/<initvar>([\s\S]*?)<\/initvar>/i)
102
236
  if (match) {
103
237
  initvarBlocks.push({
104
238
  raw: match[1].trim(),
@@ -109,24 +243,19 @@ export function parseMuvCard(cardJson) {
109
243
 
110
244
  // An <initvar> may live in the opening greeting instead. Only consulted when
111
245
  // alternate_greetings had none, so a card that already worked keeps reading
112
- // from exactly the same place it used to.
246
+ // from exactly the same place it used to. (`scenario` is included here for the
247
+ // same reason `<VariableInsert>` already scans it — the two paths were
248
+ // asymmetric.)
113
249
  if (initvarBlocks.length === 0) {
114
- for (const text of [data.first_mes, data.description]) {
115
- if (typeof text !== 'string') continue
116
- const match = text.match(/<initvar>([\s\S]*?)<\/initvar>/i)
117
- if (match) {
118
- initvarBlocks.push({
119
- raw: match[1].trim(),
120
- parsed: parseInitvar(match[1])
121
- })
122
- break
123
- }
124
- }
250
+ const hit = firstInitvarBlock(textsInGroups(candidates, 'field'))
251
+ if (hit) initvarBlocks.push(hit)
125
252
  }
126
253
 
127
254
  // Use the first initvar block as default data
128
255
  let initvarData = initvarBlocks.length > 0 ? initvarBlocks[0].parsed : {}
129
256
  let initvarRaw = initvarBlocks.map(b => b.raw)
257
+ /** Where the variable tree came from — see `schemaSource` in the return value. */
258
+ let schemaSource = initvarRaw.length > 0 ? 'initvar' : 'none'
130
259
 
131
260
  // No <initvar> anywhere → fall back to the community <VariableInsert> shape.
132
261
  if (initvarRaw.length === 0) {
@@ -134,6 +263,52 @@ export function parseMuvCard(cardJson) {
134
263
  if (inserted) {
135
264
  initvarData = inserted.data
136
265
  initvarRaw = [inserted.raw]
266
+ schemaSource = 'variable-insert'
267
+ }
268
+ }
269
+
270
+ // Still nothing → the block may only exist in the world book or in a
271
+ // tavern_helper script. This is deliberately the last resort, after
272
+ // <VariableInsert>: those two sources are full of *quoted examples* of the
273
+ // syntax, and an example is far more likely to be a bare `<initvar>` skeleton
274
+ // (which parses into junk but never throws) than a usable JSON tree.
275
+ if (initvarRaw.length === 0) {
276
+ const hit = firstInitvarBlock(textsInGroups(candidates, 'worldbook', 'script'))
277
+ if (hit) {
278
+ initvarData = hit.parsed
279
+ initvarRaw = [hit.raw]
280
+ schemaSource = 'initvar'
281
+ }
282
+ }
283
+
284
+ // Last of the last resorts: a world-book entry marked `[initvar]…` whose content is
285
+ // bare YAML with no tag at all (see worldbookInitvarEntries). Four gates:
286
+ //
287
+ // ① only after every source above came up empty — a card that already parses from
288
+ // a greeting must not change where it reads from (native `<initvar>` wins);
289
+ // ② the content must actually read as a YAML mapping (looksLikeInitvarMapping),
290
+ // not as prose/documentation;
291
+ // ③ the caller is told where the tree came from via `schemaSource`, so this
292
+ // heuristic is visible in the API response instead of being silent;
293
+ // ④ the parsed mapping must be non-empty.
294
+ //
295
+ // **`enabled` is deliberately not part of the filter.** The entry this fix exists for
296
+ // — 异世界农场's `[initvar]变量初始化勿开` — is itself `enabled: false`, and its
297
+ // comment literally says 「勿开」(do not enable). That is the convention, not an
298
+ // accident: authors keep the variable tree in a *disabled* entry so it is never
299
+ // injected into the prompt, while tooling can still read it. Skipping disabled
300
+ // entries would make this whole fallback a no-op on the only card that needs it
301
+ // (measured: repro-wb-initvar.mjs). What keeps junk out is the exact `[initvar]`
302
+ // marker plus gate ② — a disabled entry *without* that marker is never read.
303
+ if (initvarRaw.length === 0) {
304
+ for (const entry of worldbookInitvarEntries(data)) {
305
+ if (!looksLikeInitvarMapping(entry.content)) continue
306
+ const parsed = parseInitvar(entry.content)
307
+ if (!parsed || typeof parsed !== 'object' || Object.keys(parsed).length === 0) continue
308
+ initvarData = parsed
309
+ initvarRaw = [String(entry.content).trim()]
310
+ schemaSource = 'worldbook-initvar'
311
+ break
137
312
  }
138
313
  }
139
314
 
@@ -145,7 +320,8 @@ export function parseMuvCard(cardJson) {
145
320
  schemas,
146
321
  initvarData,
147
322
  initvarBlocks: initvarRaw,
148
- zodSource
323
+ zodSource,
324
+ schemaSource
149
325
  }
150
326
  }
151
327
 
@@ -180,6 +356,19 @@ function inferSchemaFromData(data, name = '') {
180
356
  description: '',
181
357
  children: []
182
358
  })
359
+ } else if (Array.isArray(value)) {
360
+ // A `- item` list. The panel edits one row per field, so the items are
361
+ // joined with newlines for display; `initvarData` keeps the real array and
362
+ // serializeInitvar writes it back as `- item` lines. Without this branch a
363
+ // list fell into the `String(value)` case and showed up as "a,b".
364
+ fields.push({
365
+ name: key,
366
+ type: 'string',
367
+ defaultValue: value.map(v => (v === null || v === undefined) ? '' : String(v)).join('\n'),
368
+ description: '',
369
+ children: [],
370
+ isList: true
371
+ })
183
372
  } else {
184
373
  fields.push({
185
374
  name: key,
package/lib/panel.html CHANGED
@@ -46,6 +46,15 @@ body { font-family: 'Segoe UI', 'Noto Sans SC', system-ui, sans-serif; backgroun
46
46
  </div>
47
47
  <script>
48
48
  let state = { cardName: '', schemas: [], initvarData: {}, edits: {}, generatedBlock: '' }
49
+ // Where the variable tree was read from, shown next to the card name. `worldbook-initvar`
50
+ // is the only *heuristic* source (a world-book entry marked `[initvar]` with bare YAML
51
+ // and no tag), so it has to be visible — otherwise a wrong guess looks exactly like a
52
+ // card that genuinely declares its variables.
53
+ const SOURCE_LABEL = { 'initvar': '<initvar> 块', 'variable-insert': '<VariableInsert>', 'worldbook-initvar': '世界书 [initvar] 条目', 'none': '' }
54
+ function badgeText(d) {
55
+ const label = SOURCE_LABEL[d.schemaSource]
56
+ return d.name + (label ? ' · ' + label : '')
57
+ }
49
58
  function handleDrop(e) { e.preventDefault(); const f = e.dataTransfer.files[0]; if (f) loadFile(f) }
50
59
  function handleFile(e) { const f = e.target.files[0]; if (f) loadFile(f) }
51
60
  function loadFile(f) { const r = new FileReader(); r.onload = () => { try { parseCard(JSON.parse(r.result)) } catch(err) { alert('JSON error: '+err.message) } }; r.readAsText(f) }
@@ -55,7 +64,7 @@ async function parseCard(json) {
55
64
  const r = await fetch('/api/muv-table/parse-card', { method:'POST', headers:{'content-type':'application/json'}, body:JSON.stringify(json) })
56
65
  const d = await r.json(); if (!d.ok) throw new Error(d.error)
57
66
  state = { cardName: d.name, schemas: d.schemas, initvarData: d.initvarData, edits: {}, generatedBlock: '' }
58
- document.getElementById('badge').style.display = 'inline'; document.getElementById('badge').textContent = d.name
67
+ document.getElementById('badge').style.display = 'inline'; document.getElementById('badge').textContent = badgeText(d)
59
68
  document.getElementById('btnGenerate').style.display = 'inline-block'
60
69
  renderTable()
61
70
  } catch(e) { document.getElementById('main').innerHTML = '<div style="padding:12px;color:var(--danger)">⚠ '+e.message+'</div>' }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-muv-table",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "MUV 变量表格编辑器 —— 树形变量面板、宏测试浮窗(random/pick/roll)、骰子快捷栏、pick 缓存管理;支持直接读取 SillyTavern PNG 角色卡(含正则脚本、世界书、变量表)。dsh-muv-engine 的伴生插件",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -10,7 +10,7 @@
10
10
  "./lib/*": "./lib/*",
11
11
  "./package.json": "./package.json"
12
12
  },
13
- "files": ["lib", "cordis.patch.yml", "CHANGELOG.md", "README.md", "test-png-card.mjs"],
13
+ "files": ["lib", "cordis.patch.yml", "CHANGELOG.md", "README.md", "test-png-card.mjs", "test-muv-parser.mjs", "test-preset-resolve.mjs"],
14
14
  "keywords": ["dsh", "dsh-plugin", "deepseek-harness", "muv", "magvarupdate", "tavern", "variable-table"],
15
15
  "license": "PolyForm-Noncommercial-Copyleft-1.0.0",
16
16
  "dsh": {