feat: add CSV support for journal stat blocks
This commit is contained in:
parent
138c089514
commit
45cd9a01ac
|
|
@ -99,6 +99,7 @@ async function scanClientSide(): Promise<JournalCompletions> {
|
||||||
const stats: StatDef[] = [];
|
const stats: StatDef[] = [];
|
||||||
const tagRegex = /<md-dice[^>]*>\s*([\s\S]*?)\s*<\/md-dice>/gi;
|
const tagRegex = /<md-dice[^>]*>\s*([\s\S]*?)\s*<\/md-dice>/gi;
|
||||||
const statBlockRegex = /```yaml\s+role=stat\s*\n([\s\S]*?)```/gi;
|
const statBlockRegex = /```yaml\s+role=stat\s*\n([\s\S]*?)```/gi;
|
||||||
|
const statCsvRegex = /```csv\s+role=stat\s*\n([\s\S]*?)```/gi;
|
||||||
|
|
||||||
for (const filePath of paths) {
|
for (const filePath of paths) {
|
||||||
const content = await getIndexedData(filePath);
|
const content = await getIndexedData(filePath);
|
||||||
|
|
@ -164,7 +165,7 @@ async function scanClientSide(): Promise<JournalCompletions> {
|
||||||
i = j - 1;
|
i = j - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stat block scan
|
// Stat block scan (YAML)
|
||||||
statBlockRegex.lastIndex = 0;
|
statBlockRegex.lastIndex = 0;
|
||||||
let statMatch: RegExpExecArray | null;
|
let statMatch: RegExpExecArray | null;
|
||||||
while ((statMatch = statBlockRegex.exec(content)) !== null) {
|
while ((statMatch = statBlockRegex.exec(content)) !== null) {
|
||||||
|
|
@ -172,6 +173,14 @@ async function scanClientSide(): Promise<JournalCompletions> {
|
||||||
const parsed = parseStatYaml(yaml, filePath);
|
const parsed = parseStatYaml(yaml, filePath);
|
||||||
stats.push(...parsed);
|
stats.push(...parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stat block scan (CSV)
|
||||||
|
statCsvRegex.lastIndex = 0;
|
||||||
|
while ((statMatch = statCsvRegex.exec(content)) !== null) {
|
||||||
|
const csv = statMatch[1];
|
||||||
|
const parsed = parseStatCsv(csv, filePath);
|
||||||
|
stats.push(...parsed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { dice, links, sparkTables, stats };
|
return { dice, links, sparkTables, stats };
|
||||||
|
|
@ -284,6 +293,95 @@ function parseStatYaml(yaml: string, source: string): StatDef[] {
|
||||||
return defs;
|
return defs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Stat CSV parser
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a ```csv role=stat block into StatDef entries.
|
||||||
|
*
|
||||||
|
* Columns: key, label, type, scope, default, roll, target, formula, options
|
||||||
|
* Only `key` is required. `type` defaults to "number", `scope` to "player".
|
||||||
|
* `options` column uses pipe-separated values: "a|b|c".
|
||||||
|
*/
|
||||||
|
function parseStatCsv(csv: string, source: string): StatDef[] {
|
||||||
|
const lines = csv.trim().split(/\r?\n/);
|
||||||
|
if (lines.length === 0) return [];
|
||||||
|
|
||||||
|
// Parse header
|
||||||
|
const headers = lines[0].split(",").map((h) => h.trim().toLowerCase());
|
||||||
|
const idx = (name: string) => {
|
||||||
|
const i = headers.indexOf(name);
|
||||||
|
return i === -1 ? null : i;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defs: StatDef[] = [];
|
||||||
|
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const row = lines[i].trim();
|
||||||
|
if (!row || row.startsWith("#")) continue;
|
||||||
|
const cols = splitCsvRow(row);
|
||||||
|
if (cols.length === 0) continue;
|
||||||
|
|
||||||
|
const get = (name: string) => {
|
||||||
|
const colIdx = idx(name);
|
||||||
|
if (colIdx === null || colIdx >= cols.length) return undefined;
|
||||||
|
return cols[colIdx].trim() || undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const key = get("key");
|
||||||
|
if (!key) continue;
|
||||||
|
|
||||||
|
const type = (get("type") || "number") as StatDef["type"];
|
||||||
|
const scope = (get("scope") || "player") as StatDef["scope"];
|
||||||
|
|
||||||
|
let options: string[] | undefined;
|
||||||
|
const optRaw = get("options");
|
||||||
|
if (optRaw) {
|
||||||
|
options = optRaw
|
||||||
|
.split("|")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
defs.push({
|
||||||
|
key,
|
||||||
|
label: get("label") || key,
|
||||||
|
type,
|
||||||
|
scope,
|
||||||
|
default: get("default"),
|
||||||
|
roll: get("roll"),
|
||||||
|
target: get("target"),
|
||||||
|
formula: get("formula"),
|
||||||
|
options,
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return defs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Split a CSV row respecting quoted fields. */
|
||||||
|
function splitCsvRow(row: string): string[] {
|
||||||
|
const cols: string[] = [];
|
||||||
|
let current = "";
|
||||||
|
let inQuote = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < row.length; i++) {
|
||||||
|
const ch = row[i];
|
||||||
|
if (ch === '"') {
|
||||||
|
inQuote = !inQuote;
|
||||||
|
} else if (ch === "," && !inQuote) {
|
||||||
|
cols.push(current);
|
||||||
|
current = "";
|
||||||
|
} else {
|
||||||
|
current += ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cols.push(current);
|
||||||
|
return cols;
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------- Init (runs eagerly at import time) -------------------
|
// ------------------- Init (runs eagerly at import time) -------------------
|
||||||
|
|
||||||
// Using a top-level IIFE so the promise starts immediately
|
// Using a top-level IIFE so the promise starts immediately
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,11 @@ props:
|
||||||
|
|
||||||
## 属性定义语法
|
## 属性定义语法
|
||||||
|
|
||||||
在任意 `.md` 文档中插入 ` ```yaml role=stat ` 代码块:
|
支持两种格式:**YAML**(适合复杂属性)和 **CSV**(适合同质列表)。
|
||||||
|
|
||||||
|
### YAML 格式
|
||||||
|
|
||||||
|
在 `.md` 文档中插入 ` ```yaml role=stat ` 代码块:
|
||||||
|
|
||||||
```yaml role=stat
|
```yaml role=stat
|
||||||
- key: strength
|
- key: strength
|
||||||
|
|
@ -93,6 +97,40 @@ props:
|
||||||
- 暴风雨
|
- 暴风雨
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### CSV 格式
|
||||||
|
|
||||||
|
对于同质属性列表(如多个 `number` 类型的属性),CSV 更紧凑。
|
||||||
|
插入 ` ```csv role=stat ` 代码块:
|
||||||
|
|
||||||
|
```csv role=stat
|
||||||
|
key,label,type,roll
|
||||||
|
mind,心智,number,2d10+20
|
||||||
|
heart,心灵,number,2d10+20
|
||||||
|
strength,力量,number,2d10+20
|
||||||
|
speed,速度,number,2d10+20
|
||||||
|
```
|
||||||
|
|
||||||
|
CSV 列说明:
|
||||||
|
|
||||||
|
| 列 | 必填 | 默认值 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `key` | ✅ | — | 属性标识符 |
|
||||||
|
| `label` | — | key 的值 | 显示名称 |
|
||||||
|
| `type` | — | `number` | 属性类型 |
|
||||||
|
| `scope` | — | `player` | `player` 或 `global` |
|
||||||
|
| `default` | — | — | 默认值 |
|
||||||
|
| `roll` | — | — | 掷骰公式 |
|
||||||
|
| `target` | — | — | modifier 的目标 key |
|
||||||
|
| `formula` | — | — | derived 的计算公式 |
|
||||||
|
| `options` | — | — | enum 选项,用 `\|` 分隔 |
|
||||||
|
|
||||||
|
示例 — enum 属性:
|
||||||
|
|
||||||
|
```csv role=stat
|
||||||
|
key,label,type,options
|
||||||
|
weather,天气,enum,晴天\|阴天\|雨天\|暴风雨
|
||||||
|
```
|
||||||
|
|
||||||
### 关键语法说明
|
### 关键语法说明
|
||||||
|
|
||||||
- **`key`**:唯一标识符。使用纯名字(如 `strength`),不用加玩家前缀
|
- **`key`**:唯一标识符。使用纯名字(如 `strength`),不用加玩家前缀
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue