Compare commits
2 Commits
228327913e
...
0aaf81057a
| Author | SHA1 | Date |
|---|---|---|
|
|
0aaf81057a | |
|
|
faf179a4fd |
|
|
@ -22,6 +22,8 @@ ttrpg serve ./content
|
||||||
| [📖 CLI 使用说明](./docs/cli.md) | CLI 安装、命令和用法 |
|
| [📖 CLI 使用说明](./docs/cli.md) | CLI 安装、命令和用法 |
|
||||||
| [🛠️ 开发指南](./docs/development.md) | 项目结构、开发规范和构建 |
|
| [🛠️ 开发指南](./docs/development.md) | 项目结构、开发规范和构建 |
|
||||||
| [📝 Markdown 编写说明](./docs/markdown.md) | Markdown 语法和组件用法 |
|
| [📝 Markdown 编写说明](./docs/markdown.md) | Markdown 语法和组件用法 |
|
||||||
|
| [📊 CSV 编写说明](./docs/csv.md) | CSV 文件格式、字段定义、变量语法 |
|
||||||
|
| [🤖 MCP 服务器说明](./docs/mcp.md) | AI 助手集成、卡牌生成工具 |
|
||||||
|
|
||||||
## 功能概览
|
## 功能概览
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ ttrpg serve [dir] -p 3000
|
||||||
| 选项 | 说明 | 默认值 |
|
| 选项 | 说明 | 默认值 |
|
||||||
|------|------|--------|
|
|------|------|--------|
|
||||||
| `-p, --port <port>` | 端口号 | `3000` |
|
| `-p, --port <port>` | 端口号 | `3000` |
|
||||||
|
| `-h, --host <host>` | 主机地址 | `0.0.0.0` |
|
||||||
|
|
||||||
**功能:**
|
**功能:**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
| `[content]` | 内容参数 | `[2d6+d8]` |
|
| `[content]` | 内容参数 | `[2d6+d8]` |
|
||||||
| `{attrs}` | 属性对象 | `{key="attack"}` |
|
| `{attrs}` | 属性对象 | `{key="attack"}` |
|
||||||
|
|
||||||
### 嵌套指令
|
支持多个嵌套指令:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
::: container
|
::: container
|
||||||
|
|
@ -36,6 +36,40 @@
|
||||||
:::
|
:::
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 分栏布局
|
||||||
|
|
||||||
|
使用简单的分隔符语法创建多栏布局:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
|---
|
||||||
|
左侧栏内容
|
||||||
|
-|-
|
||||||
|
右侧栏内容
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
**语法说明:**
|
||||||
|
|
||||||
|
| 分隔符 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| `\|---` | 开始分栏容器 |
|
||||||
|
| `-|-` | 分隔各栏 |
|
||||||
|
| `---` | 结束分栏容器 |
|
||||||
|
|
||||||
|
**多栏示例:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
|---
|
||||||
|
第一栏
|
||||||
|
-|-
|
||||||
|
第二栏
|
||||||
|
-|--
|
||||||
|
第三栏(更宽)
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
`-|--` 中的额外 `-` 会添加 `-N` 后缀到 CSS 类名(如 `col-2`),可用于自定义宽度。
|
||||||
|
|
||||||
## 图标语法
|
## 图标语法
|
||||||
|
|
||||||
使用简单的 `:[icon-name]` 语法插入图标:
|
使用简单的 `:[icon-name]` 语法插入图标:
|
||||||
|
|
@ -273,6 +307,27 @@ label,name,description
|
||||||
:md-table[./quests.csv]{roll=true remix=true}
|
:md-table[./quests.csv]{roll=true remix=true}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**自动表格转换:**
|
||||||
|
|
||||||
|
标准 Markdown 表格会自动转换为 `md-table` 组件,当表头包含 `label` 或 `md-table-label` 列时:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
| label | name | description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| 1 | 战士 | 近战专家 |
|
||||||
|
| 2 | 法师 | 奥术施法者 |
|
||||||
|
```
|
||||||
|
|
||||||
|
自动转换为 `:md-table` 组件。
|
||||||
|
|
||||||
|
**特殊表头标识:**
|
||||||
|
|
||||||
|
| 表头 | 效果 |
|
||||||
|
|------|------|
|
||||||
|
| `label` 或 `md-table-label` | 转换为 md-table |
|
||||||
|
| `md-roll-label` 或骰子格式(如 `1d6`) | 添加 `roll=true` |
|
||||||
|
| `md-remix-label` | 添加 `roll=true remix=true` |
|
||||||
|
|
||||||
### 🃏 卡牌组件 (md-deck)
|
### 🃏 卡牌组件 (md-deck)
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
|
|
@ -318,6 +373,19 @@ layers="字段:起始行,起始列 - 结束列,字体大小"
|
||||||
:md-token[./token.png]
|
:md-token[./token.png]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 🎨 代币预览组件 (md-token-viewer)
|
||||||
|
|
||||||
|
用于 3D 预览 3MF 格式的代币模型。
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
:md-token-viewer[./token.3mf]
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能:**
|
||||||
|
- 使用 Three.js 渲染 3D 模型
|
||||||
|
- 支持鼠标拖拽旋转
|
||||||
|
- 自动旋转展示
|
||||||
|
|
||||||
### 📋 命令追踪器 (md-commander)
|
### 📋 命令追踪器 (md-commander)
|
||||||
|
|
||||||
支持命令历史和状态追踪。
|
支持命令历史和状态追踪。
|
||||||
|
|
|
||||||
17
docs/mcp.md
17
docs/mcp.md
|
|
@ -230,9 +230,9 @@ ttrpg mcp generate-card-deck \
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `deck_ensure_preview` - 确保 Markdown 预览文件存在
|
#### `deck_preview` - 保存并预览卡牌组
|
||||||
|
|
||||||
确保 CSV 对应的 Markdown 预览文件存在,如果不存在则创建。
|
保存 CSV 对应的 Markdown 预览文件并打开浏览器预览。
|
||||||
|
|
||||||
| 参数 | 类型 | 必需 | 说明 |
|
| 参数 | 类型 | 必需 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
|
|
@ -241,6 +241,15 @@ ttrpg mcp generate-card-deck \
|
||||||
| `title` | string | ✗ | 标题(可选,默认从 CSV 文件名推断) |
|
| `title` | string | ✗ | 标题(可选,默认从 CSV 文件名推断) |
|
||||||
| `description` | string | ✗ | 描述(可选) |
|
| `description` | string | ✗ | 描述(可选) |
|
||||||
|
|
||||||
|
**返回示例:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "预览文件已创建",
|
||||||
|
"preview_url": "http://localhost:3000/#/content/cards.md"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## CSV 文件格式
|
## CSV 文件格式
|
||||||
|
|
||||||
CSV 文件使用 YAML frontmatter 定义模板和配置:
|
CSV 文件使用 YAML frontmatter 定义模板和配置:
|
||||||
|
|
@ -475,7 +484,7 @@ Prompt 会引导 AI 助手配置:
|
||||||
|
|
||||||
1. **设计模板**:调用 `design-card-game` Prompt 引导设计字段结构
|
1. **设计模板**:调用 `design-card-game` Prompt 引导设计字段结构
|
||||||
2. **定义模板**:调用 `deck_frontmatter_write` 创建 CSV 和 frontmatter
|
2. **定义模板**:调用 `deck_frontmatter_write` 创建 CSV 和 frontmatter
|
||||||
3. **创建预览**:调用 `deck_ensure_preview` 创建 Markdown 预览文件
|
3. **创建预览**:调用 `deck_preview` 创建 Markdown 预览文件
|
||||||
4. **填充内容**:调用 `populate-deck` Prompt 引导生成卡牌内容
|
4. **填充内容**:调用 `populate-deck` Prompt 引导生成卡牌内容
|
||||||
5. **添加卡牌**:调用 `deck_card_crud`(action=create)添加卡牌
|
5. **添加卡牌**:调用 `deck_card_crud`(action=create)添加卡牌
|
||||||
6. **配置显示**:调用 `setup-deck-display` Prompt 配置显示参数
|
6. **配置显示**:调用 `setup-deck-display` Prompt 配置显示参数
|
||||||
|
|
@ -486,7 +495,7 @@ Prompt 会引导 AI 助手配置:
|
||||||
1. **读取模板**:调用 `deck_frontmatter_read` 获取当前配置
|
1. **读取模板**:调用 `deck_frontmatter_read` 获取当前配置
|
||||||
2. **读取卡牌**:调用 `deck_card_crud`(action=read)获取卡牌数据
|
2. **读取卡牌**:调用 `deck_card_crud`(action=read)获取卡牌数据
|
||||||
3. **修改卡牌**:调用 `deck_card_crud`(action=update)更新卡牌
|
3. **修改卡牌**:调用 `deck_card_crud`(action=update)更新卡牌
|
||||||
4. **更新预览**:调用 `deck_ensure_preview` 更新 Markdown 文件
|
4. **更新预览**:调用 `deck_preview` 更新 Markdown 文件
|
||||||
|
|
||||||
### 快捷生成
|
### 快捷生成
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,13 @@ const marked = new Marked()
|
||||||
const match = rule.exec(src);
|
const match = rule.exec(src);
|
||||||
if (match) {
|
if (match) {
|
||||||
const yamlContent = match[1]?.trim() || '';
|
const yamlContent = match[1]?.trim() || '';
|
||||||
const props = yaml.load(yamlContent) as Record<string, unknown> || {};
|
let props: Record<string, unknown> = {};
|
||||||
|
try {
|
||||||
|
props = (yaml.load(yamlContent) as Record<string, unknown>) || {};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("YAML Parse Error in code-block-yaml-tag:", e);
|
||||||
|
props = { error: "Invalid YAML content" };
|
||||||
|
}
|
||||||
|
|
||||||
// 提取 tag 名称,默认为 tag-unknown
|
// 提取 tag 名称,默认为 tag-unknown
|
||||||
const tagName = (props.tag as string) || 'tag-unknown';
|
const tagName = (props.tag as string) || 'tag-unknown';
|
||||||
|
|
|
||||||
|
|
@ -87,24 +87,17 @@ class Parser {
|
||||||
while (!this.at("NODE_START")) {
|
while (!this.at("NODE_START")) {
|
||||||
const keyTok = this.take("HEADER_KEY", "Expected node header before '---'");
|
const keyTok = this.take("HEADER_KEY", "Expected node header before '---'");
|
||||||
const valTok = this.take("HEADER_VALUE", "Expected header value");
|
const valTok = this.take("HEADER_VALUE", "Expected header value");
|
||||||
if (keyTok.text === "title") title = valTok.text.trim();
|
|
||||||
if (keyTok.text === "tags") {
|
|
||||||
const raw = valTok.text.trim();
|
|
||||||
nodeTags = raw.split(/\s+/).filter(Boolean);
|
|
||||||
}
|
|
||||||
if (keyTok.text === "when") {
|
|
||||||
// Each when: header adds one condition (can have multiple when: headers)
|
|
||||||
const raw = valTok.text.trim();
|
|
||||||
whenConditions.push(raw);
|
|
||||||
}
|
|
||||||
// Capture &css{ ... } styles in any header value
|
// Capture &css{ ... } styles in any header value
|
||||||
const rawVal = valTok.text.trim();
|
const rawVal = valTok.text.trim();
|
||||||
|
let finalVal = valTok.text;
|
||||||
if (rawVal.startsWith("&css{")) {
|
if (rawVal.startsWith("&css{")) {
|
||||||
// Collect until closing '}' possibly spanning multiple lines before '---'
|
// Collect until closing '}' possibly spanning multiple lines before '---'
|
||||||
let cssContent = rawVal.replace(/^&css\{/, "");
|
let cssContent = rawVal.replace(/^&css\{/, "");
|
||||||
let closed = cssContent.includes("}");
|
let closed = cssContent.includes("}");
|
||||||
if (closed) {
|
if (closed) {
|
||||||
cssContent = cssContent.split("}")[0];
|
cssContent = cssContent.split("}")[0];
|
||||||
|
finalVal = rawVal.replace(/^&css\{[^}]*\}/, "").trim();
|
||||||
} else {
|
} else {
|
||||||
// Consume subsequent TEXT or HEADER_VALUE tokens until we find a '}'
|
// Consume subsequent TEXT or HEADER_VALUE tokens until we find a '}'
|
||||||
while (!this.at("NODE_START") && !this.at("EOF")) {
|
while (!this.at("NODE_START") && !this.at("EOF")) {
|
||||||
|
|
@ -114,6 +107,7 @@ class Parser {
|
||||||
if (t.includes("}")) {
|
if (t.includes("}")) {
|
||||||
cssContent += (cssContent ? "\n" : "") + t.split("}")[0];
|
cssContent += (cssContent ? "\n" : "") + t.split("}")[0];
|
||||||
closed = true;
|
closed = true;
|
||||||
|
finalVal = t.split("}").slice(1).join("}").trim();
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
cssContent += (cssContent ? "\n" : "") + t;
|
cssContent += (cssContent ? "\n" : "") + t;
|
||||||
|
|
@ -127,7 +121,18 @@ class Parser {
|
||||||
}
|
}
|
||||||
nodeCss = (cssContent || "").trim();
|
nodeCss = (cssContent || "").trim();
|
||||||
}
|
}
|
||||||
headers[keyTok.text] = valTok.text;
|
|
||||||
|
if (keyTok.text === "title") title = finalVal.trim();
|
||||||
|
if (keyTok.text === "tags") {
|
||||||
|
const raw = finalVal.trim();
|
||||||
|
nodeTags = raw.split(/\s+/).filter(Boolean);
|
||||||
|
}
|
||||||
|
if (keyTok.text === "when") {
|
||||||
|
// Each when: header adds one condition (can have multiple when: headers)
|
||||||
|
const raw = finalVal.trim();
|
||||||
|
whenConditions.push(raw);
|
||||||
|
}
|
||||||
|
headers[keyTok.text] = finalVal;
|
||||||
// allow empty lines
|
// allow empty lines
|
||||||
while (this.at("EMPTY")) this.i++;
|
while (this.at("EMPTY")) this.i++;
|
||||||
}
|
}
|
||||||
|
|
@ -156,6 +161,26 @@ class Parser {
|
||||||
while (this.at("EMPTY")) this.i++;
|
while (this.at("EMPTY")) this.i++;
|
||||||
if (this.at(endType) || this.at("EOF")) break;
|
if (this.at(endType) || this.at("EOF")) break;
|
||||||
|
|
||||||
|
// Handle plain indentation seamlessly within blocks
|
||||||
|
if (this.at("INDENT")) {
|
||||||
|
this.take("INDENT");
|
||||||
|
while (!this.at("DEDENT") && !this.at(endType) && !this.at("EOF")) {
|
||||||
|
while (this.at("EMPTY")) this.i++;
|
||||||
|
if (this.at("DEDENT") || this.at(endType) || this.at("EOF")) break;
|
||||||
|
|
||||||
|
if (this.at("OPTION")) {
|
||||||
|
out.push(this.parseOptionGroup());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(this.parseStatement());
|
||||||
|
}
|
||||||
|
if (this.at("DEDENT")) {
|
||||||
|
this.take("DEDENT");
|
||||||
|
while (this.at("EMPTY")) this.i++;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.at("OPTION")) {
|
if (this.at("OPTION")) {
|
||||||
out.push(this.parseOptionGroup());
|
out.push(this.parseOptionGroup());
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue