建立前端与 Tauri 桌面端的首个版本提交,包含核心编辑器、项目文件读写、测试与构建配置。 补充 Git 忽略规则和换行规范,排除依赖、构建产物、本地运行日志与临时验证文件,方便在其他电脑继续开发。
2945 lines
52 KiB
Markdown
2945 lines
52 KiB
Markdown
# BlockFlow Workbench 产品与开发规格文档
|
||
|
||
> 文档用途:本文档用于指导 Codex 或其他代码生成/辅助开发工具进行应用开发。
|
||
> 项目定位:个人高级生产力工具,本地桌面端,可视化文本结构编排与模板渲染工具。
|
||
> 当前版本目标:v0.1 可用原型。
|
||
> 重要约束:不引入 AI 能力,不做云协作,不做企业低代码平台,不做传统无限画布节点图优先的产品。
|
||
|
||
---
|
||
|
||
## 0. 总览
|
||
|
||
BlockFlow Workbench 是一款面向个人高级生产力场景的桌面端文本结构编排工具。它允许用户通过接近 Markdown 的轻量 DSL 快速创建变量、条件、循环、容器、片段引用等结构化块,并以可视化块/容器嵌套的方式维护复杂文本模板。最终,模板可以在输入 JSON 数据后被纯函数式渲染为文本、Markdown、JSON、YAML、HTML 或自定义扩展名文本。
|
||
|
||
它不是普通 Markdown 编辑器,也不是 Notion,也不是传统低代码平台。它的核心是:
|
||
|
||
```text
|
||
高级文本编辑器体验
|
||
+ 可视化块系统
|
||
+ 容器嵌套
|
||
+ 轻量模板 DSL
|
||
+ JSON 数据驱动
|
||
+ 自动表单辅助
|
||
+ 实时预览
|
||
+ 调试日志
|
||
+ 本地项目文件夹
|
||
```
|
||
|
||
一句话定义:
|
||
|
||
> BlockFlow Workbench 是一款本地化、面向高级用户的结构化文本生成 IDE。
|
||
|
||
---
|
||
|
||
## 1. 产品目标
|
||
|
||
### 1.1 核心目标
|
||
|
||
本应用的目标是帮助用户构建、维护和复用复杂文本结构。用户可以将常用文本、变量、条件逻辑、循环结构、输出格式、文档骨架、Prompt 片段、代码片段、角色设定段落、邮件模板、报告模板等统一抽象为可组合的 BlockFlow 模板。
|
||
|
||
最终应实现以下体验:
|
||
|
||
1. 用户可以像写 Markdown 一样快速输入文本。
|
||
2. 输入 `{变量名}` 后,文本立即转换为可视化变量胶囊。
|
||
3. 输入 `@if condition` 并回车后,自动转换为条件容器。
|
||
4. 输入 `@for item in items` 并回车后,自动转换为循环容器。
|
||
5. 输入 `/` 打开命令菜单,插入容器、变量、条件、循环、片段引用、注释、输出配置等块。
|
||
6. 用户可以选中已有文本,将其包裹为容器、条件、循环,或保存为片段。
|
||
7. 用户可以使用 JSON 数据集实时渲染模板输出。
|
||
8. 用户可以查看缺失变量、条件命中、循环展开、片段引用等调试信息。
|
||
9. 用户可以将常用结构保存为片段,并在其他模板中保持引用。
|
||
10. 用户可以导出单个渲染结果文件或复制最终输出到剪贴板。
|
||
|
||
### 1.2 长期价值
|
||
|
||
长期来看,BlockFlow Workbench 不只是一个模板编辑器,而是一个“个人文本组件库”和“文本生产工作台”。用户长期积累后,将拥有自己的:
|
||
|
||
- 常用表达库
|
||
- Prompt 组件库
|
||
- 文档结构库
|
||
- 角色/世界观设定片段库
|
||
- 代码注释/配置/文档模板库
|
||
- 邮件、报告、合同、客服话术模板库
|
||
- 条件逻辑和循环文本结构库
|
||
|
||
它的核心价值不是一次性生成文本,而是将复杂文本结构资产化、组件化、可维护化。
|
||
|
||
---
|
||
|
||
## 2. 非目标 / 暂不实现内容
|
||
|
||
v0.1 必须保持边界清晰,避免过早膨胀。
|
||
|
||
### 2.1 明确不做
|
||
|
||
v0.1 不做:
|
||
|
||
- AI 生成能力
|
||
- AI Prompt 自动优化
|
||
- 云端同步
|
||
- 多人协作
|
||
- 用户账号系统
|
||
- 权限系统
|
||
- 插件市场
|
||
- 企业低代码平台式工作流
|
||
- 任意 JavaScript 执行
|
||
- 外部命令执行
|
||
- 网络请求节点
|
||
- 文件读写节点
|
||
- 异步渲染副作用
|
||
- 传统无限画布节点连线作为主交互
|
||
- 内置完整 Git 客户端
|
||
- 内置版本历史快照系统
|
||
- 批量导出
|
||
- 多文件生成
|
||
- 目录生成
|
||
- SQLite 主存储
|
||
|
||
### 2.2 可以后置的能力
|
||
|
||
以下能力可在 v0.2 或更晚版本考虑:
|
||
|
||
- 完整插槽系统
|
||
- 片段 props 的复杂 UI
|
||
- 批量导出
|
||
- 多文件输出
|
||
- 目录生成
|
||
- SQLite 缓存索引
|
||
- 依赖图谱可视化
|
||
- 完整表单生成器
|
||
- 插件系统
|
||
- 源码模式高级编辑
|
||
- 更复杂表达式函数库
|
||
- 模板市场/模板包管理
|
||
|
||
---
|
||
|
||
## 3. 用户定位
|
||
|
||
### 3.1 第一目标用户
|
||
|
||
第一目标用户是开发者本人,或者类似的高级个人生产力用户。
|
||
|
||
这类用户具备以下特征:
|
||
|
||
- 能接受一定学习成本
|
||
- 喜欢结构化、工程化、可维护的工具
|
||
- 偏好本地文件、项目目录、Git 管理
|
||
- 熟悉 JSON、Markdown、代码编辑器风格界面
|
||
- 需要生成多种类型的文本:代码、文档、Prompt、创作文本、邮件、报告、设定等
|
||
- 不需要软件为普通大众降低到极致简单
|
||
|
||
### 3.2 产品气质
|
||
|
||
产品应当更像:
|
||
|
||
```text
|
||
VS Code
|
||
+ Markdown 编辑器
|
||
+ 可视化结构块系统
|
||
+ 文本模板渲染器
|
||
```
|
||
|
||
不应像:
|
||
|
||
```text
|
||
Notion 大留白文档
|
||
企业低代码拖拽平台
|
||
传统流程图工具
|
||
纯 Markdown 编辑器
|
||
普通剪贴板模板管理器
|
||
```
|
||
|
||
核心气质:
|
||
|
||
> 像写 Markdown 一样轻,像搭积木一样直观,像 IDE 一样可维护。
|
||
|
||
---
|
||
|
||
## 4. 核心设计原则
|
||
|
||
### 4.1 写作体验优先
|
||
|
||
主体验是高级文本编辑器,而不是传统节点图或低代码画布。用户应能连续输入、快速改写、键盘优先操作。
|
||
|
||
### 4.2 结构化能力隐藏在块系统中
|
||
|
||
软件底层具有接近低代码的结构表达能力,但界面不能显得笨重。结构应表现为可折叠、可嵌套、可拖拽的块和容器。
|
||
|
||
### 4.3 AST 是唯一真相
|
||
|
||
项目文件中保存的是自定义 BlockFlow AST,而不是 HTML、ProseMirror Doc、TipTap JSON 或其他编辑器私有结构。
|
||
|
||
```text
|
||
BlockFlow AST ←→ Editor Adapter ←→ TipTap / ProseMirror
|
||
```
|
||
|
||
渲染器、导出器、引用索引、变量收集、DSL 序列化都基于 BlockFlow AST。
|
||
|
||
### 4.4 纯函数式渲染
|
||
|
||
渲染模型必须是:
|
||
|
||
```text
|
||
模板结构 + 输入数据 = 输出文本
|
||
```
|
||
|
||
同一模板与同一数据必须得到相同输出。渲染过程不允许副作用,不允许修改外部状态,不允许文件读写、网络请求、外部命令、任意脚本执行。
|
||
|
||
### 4.5 项目文件夹是项目本体
|
||
|
||
项目以真实文件夹保存。软件的资源树只是对文件夹的增强展示。所有核心内容应可被 Git 管理、可手动查看、可手动修复。
|
||
|
||
### 4.6 可视化是为了维护,不是为了牺牲效率
|
||
|
||
可视化块应低调、高信息密度。变量胶囊、条件容器、循环容器等应帮助用户维护结构,而不能破坏文本编辑效率。
|
||
|
||
---
|
||
|
||
## 5. 产品核心概念
|
||
|
||
### 5.1 Workspace / Project
|
||
|
||
项目文件夹是一个 BlockFlow Project。它包含模板、片段、数据集、Schema、导出文件等。
|
||
|
||
### 5.2 Template
|
||
|
||
模板是可渲染入口。一个模板由 BlockNode 树组成。
|
||
|
||
### 5.3 Fragment
|
||
|
||
片段是可复用组件。片段与模板本质相同,都是 BlockTree,但片段通常被其他模板或片段引用。
|
||
|
||
### 5.4 Dataset
|
||
|
||
数据集是用于渲染模板的 JSON 数据。模板与数据集分离。
|
||
|
||
### 5.5 BlockNode
|
||
|
||
块级节点。用于表达段落、容器、条件、循环、片段引用、插槽、注释、输出配置等结构。
|
||
|
||
### 5.6 InlineNode
|
||
|
||
行内节点。用于表达一行内部的文本、变量、表达式、行内片段、占位符等。
|
||
|
||
### 5.7 Renderer
|
||
|
||
渲染器读取 TemplateDocument、FragmentDocuments 和 Dataset,输出 RenderResult。
|
||
|
||
### 5.8 Reference Index
|
||
|
||
引用索引用于追踪模板与片段之间的关系,包括谁引用谁、缺失片段、循环引用等。
|
||
|
||
---
|
||
|
||
## 6. v0.1 功能范围
|
||
|
||
### 6.1 v0.1 必须实现
|
||
|
||
v0.1 应至少实现:
|
||
|
||
- 创建/打开项目文件夹
|
||
- 读取 project.json
|
||
- 扫描 templates、fragments、datasets
|
||
- JSON AST 存储
|
||
- 三栏 UI
|
||
- TipTap 基础编辑器
|
||
- 普通文本段落
|
||
- 变量胶囊
|
||
- 条件容器
|
||
- 循环容器
|
||
- 普通容器
|
||
- 片段引用
|
||
- JSON 数据输入
|
||
- 实时预览
|
||
- 调试日志
|
||
- 复制输出
|
||
- 单文件导出
|
||
- 自动保存
|
||
- 外部修改检测
|
||
- 引用追踪
|
||
- 缺失变量提示
|
||
- 缺失片段提示
|
||
- 片段循环引用检测
|
||
|
||
### 6.2 v0.1 推荐实现但可降级
|
||
|
||
以下能力重要,但如果时间不足可在 v0.1 后半段实现:
|
||
|
||
- 选区包裹为容器
|
||
- 选区包裹为条件
|
||
- 选区包裹为循环
|
||
- 选区保存为片段
|
||
- DSL 源码模式
|
||
- 局部源码编辑
|
||
- 表单模式数据输入
|
||
- 输出文件名模板
|
||
|
||
### 6.3 v0.1 不必完整实现
|
||
|
||
- 完整插槽系统
|
||
- 完整片段 props 编辑 UI
|
||
- 批量导出
|
||
- 依赖图谱
|
||
- SQLite 索引缓存
|
||
- 内置历史快照
|
||
- 插件系统
|
||
|
||
---
|
||
|
||
## 7. 项目文件夹结构
|
||
|
||
### 7.1 推荐结构
|
||
|
||
```text
|
||
MyBlockFlowProject/
|
||
project.json
|
||
|
||
templates/
|
||
main.json
|
||
code-template.json
|
||
prompt-template.json
|
||
character-template.json
|
||
|
||
fragments/
|
||
common-header.json
|
||
common-footer.json
|
||
prompt-rules.json
|
||
markdown-output-rules.json
|
||
|
||
datasets/
|
||
default.json
|
||
vip-user.json
|
||
empty-list.json
|
||
debug-case.json
|
||
|
||
schemas/
|
||
common-inputs.json
|
||
|
||
exports/
|
||
latest-output.md
|
||
latest-output.txt
|
||
```
|
||
|
||
### 7.2 v0.1 最小结构
|
||
|
||
```text
|
||
MyBlockFlowProject/
|
||
project.json
|
||
templates/
|
||
fragments/
|
||
datasets/
|
||
```
|
||
|
||
### 7.3 project.json 示例
|
||
|
||
```json
|
||
{
|
||
"id": "project_main",
|
||
"name": "My BlockFlow Project",
|
||
"version": "0.1.0",
|
||
"createdAt": "2026-05-24T00:00:00.000Z",
|
||
"updatedAt": "2026-05-24T00:00:00.000Z",
|
||
"entryTemplateId": "template_main"
|
||
}
|
||
```
|
||
|
||
### 7.4 文件原则
|
||
|
||
1. 所有核心内容使用 JSON 保存。
|
||
2. 一个模板一个文件。
|
||
3. 一个片段一个文件。
|
||
4. 一个数据集一个文件。
|
||
5. 项目可被 Git 管理。
|
||
6. 不依赖数据库也能打开项目。
|
||
7. 不保存编辑器私有结构作为主数据。
|
||
8. 可以从 JSON 完整恢复项目。
|
||
9. 外部手动修改文件后,软件刷新应能识别。
|
||
10. 软件内新建/重命名/删除资源时,应真实操作文件系统。
|
||
|
||
---
|
||
|
||
## 8. 技术栈
|
||
|
||
v0.1 技术栈锁定为:
|
||
|
||
```text
|
||
桌面壳:Tauri
|
||
语言:TypeScript
|
||
前端:React
|
||
编辑器:TipTap / ProseMirror
|
||
状态管理:Zustand
|
||
样式:Tailwind CSS
|
||
主存储:项目文件夹 + JSON
|
||
缓存索引:内存索引,暂不引入 SQLite
|
||
```
|
||
|
||
### 8.1 Tauri
|
||
|
||
用于桌面壳、本地文件访问、窗口管理。相比 Electron 更轻量,适合个人本地工具。
|
||
|
||
### 8.2 React + TypeScript
|
||
|
||
用于复杂界面、检查器、资源树、编辑器外壳、状态管理。
|
||
|
||
### 8.3 TipTap / ProseMirror
|
||
|
||
用于编辑器视图层,处理光标、选区、输入法、快捷键、撤销重做、自定义行内节点、自定义块级节点等。
|
||
|
||
重要约束:TipTap Doc 不是项目文件格式。必须通过 Adapter 与 BlockFlow AST 转换。
|
||
|
||
### 8.4 Zustand
|
||
|
||
管理应用状态,包括当前项目、打开文件、选中块、当前数据集、保存状态、右侧面板 Tab 等。
|
||
|
||
### 8.5 Tailwind CSS
|
||
|
||
用于快速构建高信息密度、低装饰、深色优先的 UI。
|
||
|
||
---
|
||
|
||
## 9. 推荐代码目录结构
|
||
|
||
```text
|
||
src/
|
||
app/
|
||
App.tsx
|
||
routes/
|
||
store/
|
||
projectStore.ts
|
||
editorStore.ts
|
||
renderStore.ts
|
||
uiStore.ts
|
||
layout/
|
||
MainLayout.tsx
|
||
TopBar.tsx
|
||
StatusBar.tsx
|
||
LeftResourcePanel.tsx
|
||
RightInspectorPanel.tsx
|
||
|
||
core/
|
||
types/
|
||
common.ts
|
||
project.ts
|
||
document.ts
|
||
dataset.ts
|
||
nodes.ts
|
||
schema.ts
|
||
output.ts
|
||
render.ts
|
||
reference.ts
|
||
renderer/
|
||
renderTemplate.ts
|
||
renderBlock.ts
|
||
renderInline.ts
|
||
renderContext.ts
|
||
missingVariable.ts
|
||
expression/
|
||
tokenizer.ts
|
||
parser.ts
|
||
evaluator.ts
|
||
types.ts
|
||
reference/
|
||
buildReferenceIndex.ts
|
||
detectReferenceCycles.ts
|
||
dsl/
|
||
parseDsl.ts
|
||
serializeDsl.ts
|
||
schema/
|
||
collectVariables.ts
|
||
inferInputSchema.ts
|
||
|
||
editor/
|
||
BlockFlowEditor.tsx
|
||
adapter/
|
||
astToTiptap.ts
|
||
tiptapToAst.ts
|
||
extensions/
|
||
VariableInlineExtension.ts
|
||
ConditionBlockExtension.ts
|
||
LoopBlockExtension.ts
|
||
ContainerBlockExtension.ts
|
||
FragmentRefExtension.ts
|
||
commands/
|
||
insertVariable.ts
|
||
wrapSelectionAsCondition.ts
|
||
wrapSelectionAsLoop.ts
|
||
saveSelectionAsFragment.ts
|
||
inputRules/
|
||
variableInputRule.ts
|
||
conditionInputRule.ts
|
||
loopInputRule.ts
|
||
components/
|
||
VariablePill.tsx
|
||
ConditionBlockView.tsx
|
||
LoopBlockView.tsx
|
||
ContainerBlockView.tsx
|
||
FragmentRefView.tsx
|
||
|
||
project/
|
||
createProject.ts
|
||
openProject.ts
|
||
scanProject.ts
|
||
readDocument.ts
|
||
writeDocument.ts
|
||
watchProjectFiles.ts
|
||
validateProject.ts
|
||
fileNaming.ts
|
||
|
||
ui/
|
||
resource-tree/
|
||
inspector/
|
||
data-panel/
|
||
preview-panel/
|
||
debug-panel/
|
||
reference-panel/
|
||
command-palette/
|
||
context-menu/
|
||
|
||
utils/
|
||
id.ts
|
||
path.ts
|
||
json.ts
|
||
debounce.ts
|
||
```
|
||
|
||
---
|
||
|
||
## 10. TypeScript 核心类型定义
|
||
|
||
以下类型是 v0.1 的基础数据模型。Codex 开发时应以这些类型为主线,不要让 UI 私有类型污染核心模型。
|
||
|
||
### 10.1 common.ts
|
||
|
||
```ts
|
||
export type ID = string
|
||
export type FilePath = string
|
||
export type ISODateString = string
|
||
|
||
export type BlockId = ID
|
||
export type TemplateId = ID
|
||
export type FragmentId = ID
|
||
export type DataSetId = ID
|
||
```
|
||
|
||
### 10.2 project.ts
|
||
|
||
```ts
|
||
import type { FilePath, ISODateString, TemplateId } from "./common"
|
||
|
||
export type BlockFlowProject = {
|
||
id: string
|
||
name: string
|
||
version: string
|
||
|
||
createdAt: ISODateString
|
||
updatedAt: ISODateString
|
||
|
||
entryTemplateId?: TemplateId
|
||
|
||
paths: {
|
||
root: FilePath
|
||
templatesDir: FilePath
|
||
fragmentsDir: FilePath
|
||
datasetsDir: FilePath
|
||
exportsDir?: FilePath
|
||
}
|
||
}
|
||
```
|
||
|
||
### 10.3 document.ts
|
||
|
||
```ts
|
||
import type { FilePath, ISODateString, TemplateId, FragmentId } from "./common"
|
||
import type { BlockNode } from "./nodes"
|
||
import type { InputSchema } from "./schema"
|
||
import type { OutputConfig } from "./output"
|
||
|
||
export type TemplateDocument = {
|
||
kind: "template"
|
||
|
||
id: TemplateId
|
||
name: string
|
||
description?: string
|
||
|
||
filePath?: FilePath
|
||
tags?: string[]
|
||
|
||
createdAt?: ISODateString
|
||
updatedAt?: ISODateString
|
||
|
||
inputSchema?: InputSchema
|
||
outputConfig?: OutputConfig
|
||
|
||
children: BlockNode[]
|
||
}
|
||
|
||
export type FragmentDocument = {
|
||
kind: "fragment"
|
||
|
||
id: FragmentId
|
||
name: string
|
||
description?: string
|
||
|
||
filePath?: FilePath
|
||
tags?: string[]
|
||
|
||
createdAt?: ISODateString
|
||
updatedAt?: ISODateString
|
||
|
||
inputSchema?: InputSchema
|
||
|
||
children: BlockNode[]
|
||
}
|
||
```
|
||
|
||
### 10.4 dataset.ts
|
||
|
||
```ts
|
||
import type { DataSetId, FilePath, ISODateString } from "./common"
|
||
|
||
export type DataSetDocument = {
|
||
kind: "dataset"
|
||
|
||
id: DataSetId
|
||
name: string
|
||
description?: string
|
||
|
||
filePath?: FilePath
|
||
tags?: string[]
|
||
|
||
createdAt?: ISODateString
|
||
updatedAt?: ISODateString
|
||
|
||
data: Record<string, unknown>
|
||
}
|
||
```
|
||
|
||
### 10.5 nodes.ts
|
||
|
||
```ts
|
||
import type { BlockId, FragmentId } from "./common"
|
||
import type { OutputConfig } from "./output"
|
||
|
||
export type BlockNode =
|
||
| ParagraphBlock
|
||
| ContainerBlock
|
||
| ConditionBlock
|
||
| LoopBlock
|
||
| FragmentRefBlock
|
||
| SlotBlock
|
||
| CommentBlock
|
||
| OutputBlock
|
||
|
||
export type InlineNode =
|
||
| TextInline
|
||
| VariableInline
|
||
| ExpressionInline
|
||
| InlineFragmentRef
|
||
| PlaceholderInline
|
||
|
||
export type BaseBlock = {
|
||
id: BlockId
|
||
type: string
|
||
|
||
name?: string
|
||
enabled?: boolean
|
||
|
||
ui?: BlockUiState
|
||
rendering?: RenderingOptions
|
||
}
|
||
|
||
export type BlockUiState = {
|
||
collapsed?: boolean
|
||
selected?: boolean
|
||
color?: string
|
||
note?: string
|
||
}
|
||
|
||
export type RenderingOptions = {
|
||
prefix?: string
|
||
suffix?: string
|
||
separator?: string
|
||
|
||
trimStart?: boolean
|
||
trimEnd?: boolean
|
||
|
||
preserveEmptyLine?: boolean
|
||
}
|
||
|
||
export type ParagraphBlock = BaseBlock & {
|
||
type: "paragraph"
|
||
inlines: InlineNode[]
|
||
}
|
||
|
||
export type TextInline = {
|
||
type: "text"
|
||
content: string
|
||
}
|
||
|
||
export type VariableInline = {
|
||
type: "variable"
|
||
path: string
|
||
|
||
displayName?: string
|
||
fallback?: unknown
|
||
format?: string
|
||
|
||
required?: boolean
|
||
missingStrategy?: MissingVariableStrategy
|
||
}
|
||
|
||
export type ExpressionInline = {
|
||
type: "expression"
|
||
expression: string
|
||
fallback?: unknown
|
||
format?: string
|
||
}
|
||
|
||
export type InlineFragmentRef = {
|
||
type: "inlineFragment"
|
||
fragmentId: FragmentId
|
||
props?: Record<string, unknown>
|
||
}
|
||
|
||
export type PlaceholderInline = {
|
||
type: "placeholder"
|
||
name: string
|
||
description?: string
|
||
}
|
||
|
||
export type MissingVariableStrategy =
|
||
| "global"
|
||
| "error"
|
||
| "keep-placeholder"
|
||
| "empty-string"
|
||
| "fallback"
|
||
|
||
export type ContainerBlock = BaseBlock & {
|
||
type: "container"
|
||
name: string
|
||
children: BlockNode[]
|
||
}
|
||
|
||
export type ConditionBlock = BaseBlock & {
|
||
type: "condition"
|
||
expression: string
|
||
children: BlockNode[]
|
||
elseChildren?: BlockNode[]
|
||
}
|
||
|
||
export type LoopBlock = BaseBlock & {
|
||
type: "loop"
|
||
source: string
|
||
itemName: string
|
||
indexName?: string
|
||
children: BlockNode[]
|
||
emptyChildren?: BlockNode[]
|
||
separator?: string
|
||
}
|
||
|
||
export type FragmentRefBlock = BaseBlock & {
|
||
type: "fragmentRef"
|
||
fragmentId: FragmentId
|
||
props?: Record<string, unknown>
|
||
fills?: Record<string, BlockNode[]>
|
||
}
|
||
|
||
export type SlotBlock = BaseBlock & {
|
||
type: "slot"
|
||
slotName: string
|
||
fallbackChildren?: BlockNode[]
|
||
}
|
||
|
||
export type CommentBlock = BaseBlock & {
|
||
type: "comment"
|
||
content: string
|
||
multiline?: boolean
|
||
}
|
||
|
||
export type OutputBlock = BaseBlock & {
|
||
type: "output"
|
||
config: OutputConfig
|
||
}
|
||
```
|
||
|
||
### 10.6 output.ts
|
||
|
||
```ts
|
||
export type OutputFormat =
|
||
| "text"
|
||
| "markdown"
|
||
| "json"
|
||
| "yaml"
|
||
| "html"
|
||
| "custom"
|
||
|
||
export type OutputConfig = {
|
||
format: OutputFormat
|
||
|
||
customExtension?: string
|
||
fileNameTemplate?: string
|
||
|
||
missingVariableStrategy?: ExportMissingVariableStrategy
|
||
|
||
trimFinalOutput?: boolean
|
||
ensureTrailingNewline?: boolean
|
||
}
|
||
|
||
export type ExportMissingVariableStrategy =
|
||
| "error"
|
||
| "keep-placeholder"
|
||
| "empty-string"
|
||
```
|
||
|
||
### 10.7 schema.ts
|
||
|
||
```ts
|
||
export type InputSchema = {
|
||
fields: InputField[]
|
||
}
|
||
|
||
export type InputField = {
|
||
path: string
|
||
type: InputFieldType
|
||
|
||
label?: string
|
||
description?: string
|
||
|
||
required?: boolean
|
||
defaultValue?: unknown
|
||
|
||
enumOptions?: string[]
|
||
}
|
||
|
||
export type InputFieldType =
|
||
| "string"
|
||
| "number"
|
||
| "boolean"
|
||
| "array"
|
||
| "object"
|
||
| "date"
|
||
| "unknown"
|
||
```
|
||
|
||
### 10.8 render.ts
|
||
|
||
```ts
|
||
import type { TemplateId, FragmentId, DataSetId, BlockId } from "./common"
|
||
import type { TemplateDocument, FragmentDocument } from "./document"
|
||
import type { DataSetDocument } from "./dataset"
|
||
import type { OutputConfig } from "./output"
|
||
|
||
export type RenderInput = {
|
||
template: TemplateDocument
|
||
fragments: Record<FragmentId, FragmentDocument>
|
||
dataset?: DataSetDocument
|
||
data?: Record<string, unknown>
|
||
options?: RenderOptions
|
||
}
|
||
|
||
export type RenderOptions = {
|
||
outputConfig?: OutputConfig
|
||
mode?: RenderMode
|
||
targetBlockId?: BlockId
|
||
maxFragmentDepth?: number
|
||
}
|
||
|
||
export type RenderMode = "preview" | "export"
|
||
|
||
export type RenderResult = {
|
||
ok: boolean
|
||
output: string
|
||
logs: RenderLog[]
|
||
warnings: RenderWarning[]
|
||
errors: RenderError[]
|
||
stats: RenderStats
|
||
}
|
||
|
||
export type RenderStats = {
|
||
usedVariables: string[]
|
||
missingVariables: string[]
|
||
usedFragments: FragmentId[]
|
||
missingFragments: FragmentId[]
|
||
renderedBlockCount: number
|
||
}
|
||
|
||
export type RenderLog =
|
||
| VariableRenderLog
|
||
| ConditionRenderLog
|
||
| LoopRenderLog
|
||
| FragmentRenderLog
|
||
| SlotRenderLog
|
||
| OutputRenderLog
|
||
|
||
export type VariableRenderLog = {
|
||
type: "variable"
|
||
blockId?: BlockId
|
||
path: string
|
||
resolved: boolean
|
||
value?: unknown
|
||
fallbackUsed?: boolean
|
||
}
|
||
|
||
export type ConditionRenderLog = {
|
||
type: "condition"
|
||
blockId: BlockId
|
||
expression: string
|
||
result: boolean
|
||
}
|
||
|
||
export type LoopRenderLog = {
|
||
type: "loop"
|
||
blockId: BlockId
|
||
source: string
|
||
count: number
|
||
}
|
||
|
||
export type FragmentRenderLog = {
|
||
type: "fragment"
|
||
blockId: BlockId
|
||
fragmentId: FragmentId
|
||
resolved: boolean
|
||
}
|
||
|
||
export type SlotRenderLog = {
|
||
type: "slot"
|
||
blockId: BlockId
|
||
slotName: string
|
||
filled: boolean
|
||
fallbackUsed?: boolean
|
||
}
|
||
|
||
export type OutputRenderLog = {
|
||
type: "output"
|
||
format: string
|
||
}
|
||
|
||
export type RenderWarning = {
|
||
code: RenderWarningCode
|
||
message: string
|
||
blockId?: BlockId
|
||
path?: string
|
||
fragmentId?: FragmentId
|
||
}
|
||
|
||
export type RenderError = {
|
||
code: RenderErrorCode
|
||
message: string
|
||
blockId?: BlockId
|
||
path?: string
|
||
fragmentId?: FragmentId
|
||
cause?: unknown
|
||
}
|
||
|
||
export type RenderWarningCode =
|
||
| "MISSING_VARIABLE"
|
||
| "FALLBACK_USED"
|
||
| "EMPTY_LOOP"
|
||
| "UNFILLED_SLOT"
|
||
| "UNKNOWN_TYPE"
|
||
|
||
export type RenderErrorCode =
|
||
| "INVALID_EXPRESSION"
|
||
| "MISSING_VARIABLE_EXPORT"
|
||
| "MISSING_FRAGMENT"
|
||
| "FRAGMENT_CYCLE"
|
||
| "LOOP_SOURCE_NOT_ARRAY"
|
||
| "INVALID_BLOCK"
|
||
| "RENDER_DEPTH_EXCEEDED"
|
||
```
|
||
|
||
### 10.9 reference.ts
|
||
|
||
```ts
|
||
import type { TemplateId, FragmentId, FilePath } from "./common"
|
||
|
||
export type DocumentRefId = TemplateId | FragmentId
|
||
|
||
export type ReferenceIndex = {
|
||
outgoing: Record<DocumentRefId, ReferenceItem[]>
|
||
incoming: Record<DocumentRefId, ReferenceItem[]>
|
||
missingFragments: MissingFragmentRef[]
|
||
cycles: ReferenceCycle[]
|
||
}
|
||
|
||
export type ReferenceItem = {
|
||
fromId: DocumentRefId
|
||
fromPath?: FilePath
|
||
toFragmentId: FragmentId
|
||
blockId?: string
|
||
}
|
||
|
||
export type MissingFragmentRef = {
|
||
fromId: DocumentRefId
|
||
fromPath?: FilePath
|
||
fragmentId: FragmentId
|
||
blockId?: string
|
||
}
|
||
|
||
export type ReferenceCycle = {
|
||
chain: DocumentRefId[]
|
||
}
|
||
```
|
||
|
||
### 10.10 editorState.ts
|
||
|
||
该类型不写入项目文件,只用于前端状态。
|
||
|
||
```ts
|
||
import type { BlockId, DataSetId, TemplateId, FragmentId, FilePath } from "@/core/types/common"
|
||
|
||
export type OpenDocumentId = TemplateId | FragmentId
|
||
|
||
export type EditorState = {
|
||
currentProjectPath?: FilePath
|
||
|
||
openDocumentId?: OpenDocumentId
|
||
openDocumentKind?: "template" | "fragment"
|
||
|
||
selectedBlockId?: BlockId
|
||
currentDataSetId?: DataSetId
|
||
|
||
rightPanelTab: RightPanelTab
|
||
saveStatus: SaveStatus
|
||
externalFileStatus?: ExternalFileStatus
|
||
}
|
||
|
||
export type RightPanelTab =
|
||
| "properties"
|
||
| "data"
|
||
| "preview"
|
||
| "debug"
|
||
| "references"
|
||
|
||
export type SaveStatus =
|
||
| "saved"
|
||
| "dirty"
|
||
| "saving"
|
||
| "error"
|
||
|
||
export type ExternalFileStatus =
|
||
| "clean"
|
||
| "changed-externally"
|
||
| "conflict"
|
||
```
|
||
|
||
---
|
||
|
||
## 11. AST 设计规则
|
||
|
||
### 11.1 BlockNode 与 InlineNode 分离
|
||
|
||
所有普通文字都在 ParagraphBlock 中。ParagraphBlock 内部由 InlineNode 组成。
|
||
|
||
示例:
|
||
|
||
```text
|
||
你好,{user.name},你的订单是 {order.id}
|
||
```
|
||
|
||
保存为:
|
||
|
||
```json
|
||
{
|
||
"id": "block_p1",
|
||
"type": "paragraph",
|
||
"inlines": [
|
||
{ "type": "text", "content": "你好," },
|
||
{ "type": "variable", "path": "user.name" },
|
||
{ "type": "text", "content": ",你的订单是 " },
|
||
{ "type": "variable", "path": "order.id" }
|
||
]
|
||
}
|
||
```
|
||
|
||
变量必须是一等 InlineNode,而不是藏在 TextInline 的字符串里。
|
||
|
||
### 11.2 块级结构
|
||
|
||
块级结构用于组织、控制、复用文本。
|
||
|
||
典型结构:
|
||
|
||
```text
|
||
Template
|
||
└─ BlockNode[]
|
||
├─ ParagraphBlock
|
||
├─ ContainerBlock
|
||
│ └─ BlockNode[]
|
||
├─ ConditionBlock
|
||
│ └─ BlockNode[]
|
||
├─ LoopBlock
|
||
│ └─ BlockNode[]
|
||
├─ FragmentRefBlock
|
||
├─ SlotBlock
|
||
├─ CommentBlock
|
||
└─ OutputBlock
|
||
```
|
||
|
||
### 11.3 enabled 字段
|
||
|
||
所有 BaseBlock 可拥有 `enabled?: boolean`。若 `enabled === false`,渲染器应跳过该块。
|
||
|
||
### 11.4 ui 字段
|
||
|
||
`ui` 可保存折叠状态、颜色、备注等 UI 信息,但不参与核心渲染逻辑。
|
||
|
||
### 11.5 rendering 字段
|
||
|
||
`rendering` 用于控制 prefix、suffix、separator、trim 等渲染细节。v0.1 可只实现最基础行为,但数据结构应预留。
|
||
|
||
---
|
||
|
||
## 12. DSL 设计
|
||
|
||
DSL 表层像 Markdown,逻辑像轻量模板语言。
|
||
|
||
### 12.1 语法总表
|
||
|
||
```text
|
||
{name} 变量
|
||
{user.name} 路径变量
|
||
{user.name ?? "未命名"} 带 fallback 的表达式/变量
|
||
|
||
# 容器名 普通容器
|
||
|
||
@if condition 条件开始
|
||
@else 条件否则
|
||
@endif 条件结束
|
||
|
||
@for item in items 循环开始
|
||
@endfor 循环结束
|
||
|
||
@use fragmentName 引用片段
|
||
|
||
@slot name 定义插槽
|
||
@fill name 填充插槽
|
||
@endfill 结束填充
|
||
|
||
// comment 单行注释
|
||
|
||
@comment 多行注释开始
|
||
@endcomment 多行注释结束
|
||
|
||
@output markdown 输出配置
|
||
```
|
||
|
||
### 12.2 变量
|
||
|
||
```text
|
||
{name}
|
||
{user.name}
|
||
{items[0].title}
|
||
```
|
||
|
||
可视化后显示为变量胶囊:
|
||
|
||
```text
|
||
[user.name]
|
||
```
|
||
|
||
### 12.3 容器
|
||
|
||
```text
|
||
# 角色基础信息
|
||
这里写角色姓名、年龄、身份等内容。
|
||
```
|
||
|
||
转换为 ContainerBlock。
|
||
|
||
### 12.4 条件
|
||
|
||
```text
|
||
@if user.isVip
|
||
感谢你作为 VIP 用户长期支持我们。
|
||
@endif
|
||
```
|
||
|
||
支持 else:
|
||
|
||
```text
|
||
@if user.type == "developer"
|
||
输出技术版本。
|
||
@else
|
||
输出普通版本。
|
||
@endif
|
||
```
|
||
|
||
### 12.5 循环
|
||
|
||
```text
|
||
@for item in items
|
||
- {item.title}: {item.price}
|
||
@endfor
|
||
```
|
||
|
||
转换为 LoopBlock。
|
||
|
||
### 12.6 片段引用
|
||
|
||
```text
|
||
@use common-footer
|
||
```
|
||
|
||
转换为 FragmentRefBlock。
|
||
|
||
### 12.7 插槽
|
||
|
||
v0.1 可预留,不必完整实现。
|
||
|
||
```text
|
||
@slot content
|
||
```
|
||
|
||
填充语法:
|
||
|
||
```text
|
||
@use wrapper
|
||
@fill content
|
||
这里是填充内容。
|
||
@endfill
|
||
@enduse
|
||
```
|
||
|
||
### 12.8 注释
|
||
|
||
```text
|
||
// 这是单行注释
|
||
```
|
||
|
||
或:
|
||
|
||
```text
|
||
@comment
|
||
这里是多行注释。
|
||
@endcomment
|
||
```
|
||
|
||
注释不参与输出。
|
||
|
||
---
|
||
|
||
## 13. 输入转换规则
|
||
|
||
### 13.1 转换策略
|
||
|
||
采用混合策略:
|
||
|
||
```text
|
||
变量:实时转换
|
||
块级语法:回车转换
|
||
复杂结构:命令确认
|
||
```
|
||
|
||
### 13.2 变量实时转换
|
||
|
||
用户输入:
|
||
|
||
```text
|
||
{user.name}
|
||
```
|
||
|
||
当输入 `}` 后立即转换为变量胶囊。
|
||
|
||
### 13.3 块级语法回车转换
|
||
|
||
用户输入:
|
||
|
||
```text
|
||
@if user.isVip
|
||
```
|
||
|
||
按 Enter 后转换为条件容器。
|
||
|
||
用户输入:
|
||
|
||
```text
|
||
@for item in items
|
||
```
|
||
|
||
按 Enter 后转换为循环容器。
|
||
|
||
### 13.4 复杂结构命令确认
|
||
|
||
输入 `/` 打开命令菜单:
|
||
|
||
```text
|
||
/变量
|
||
/容器
|
||
/条件
|
||
/循环
|
||
/片段引用
|
||
/插槽
|
||
/注释
|
||
/输出
|
||
```
|
||
|
||
选择后生成对应结构。
|
||
|
||
---
|
||
|
||
## 14. 选区转块功能
|
||
|
||
选区转块是核心交互,不是附加功能。
|
||
|
||
### 14.1 v0.1 应支持
|
||
|
||
```text
|
||
选区 → 包裹为容器
|
||
选区 → 包裹为条件
|
||
选区 → 包裹为循环
|
||
选区 → 保存为片段
|
||
选区 → 转为注释
|
||
选区 → 禁用输出
|
||
```
|
||
|
||
### 14.2 包裹为条件
|
||
|
||
原始内容:
|
||
|
||
```text
|
||
这段内容只在 VIP 用户下显示。
|
||
```
|
||
|
||
执行“包裹为条件”后:
|
||
|
||
```text
|
||
▾ if 条件表达式
|
||
这段内容只在 VIP 用户下显示。
|
||
```
|
||
|
||
应自动聚焦条件表达式。
|
||
|
||
### 14.3 包裹为循环
|
||
|
||
原始内容:
|
||
|
||
```text
|
||
- {item.title}: {item.price}
|
||
```
|
||
|
||
执行“包裹为循环”后:
|
||
|
||
```text
|
||
▾ for item in list
|
||
- [item.title]: [item.price]
|
||
```
|
||
|
||
应自动聚焦 `list` 或 source 字段。
|
||
|
||
### 14.4 保存为片段
|
||
|
||
选中内容后执行“保存为片段”,弹出:
|
||
|
||
```text
|
||
片段名称
|
||
保存位置
|
||
是否替换原选区为片段引用
|
||
```
|
||
|
||
如果选择替换,原选区变为:
|
||
|
||
```text
|
||
use fragment-name
|
||
```
|
||
|
||
---
|
||
|
||
## 15. 片段系统
|
||
|
||
### 15.1 默认保持引用关系
|
||
|
||
片段引用不是复制内容,而是保持链接。
|
||
|
||
```text
|
||
@use common-footer
|
||
```
|
||
|
||
保存为:
|
||
|
||
```json
|
||
{
|
||
"type": "fragmentRef",
|
||
"fragmentId": "common-footer"
|
||
}
|
||
```
|
||
|
||
修改片段本体会影响所有引用处。
|
||
|
||
### 15.2 支持展开片段
|
||
|
||
用户可右键片段引用,选择“展开为普通内容”。展开后:
|
||
|
||
- 片段内容复制到当前模板
|
||
- 原引用关系断开
|
||
- 后续修改片段本体不影响已展开内容
|
||
|
||
### 15.3 片段状态显示
|
||
|
||
片段引用块应显示状态:
|
||
|
||
```text
|
||
use common-footer ✓
|
||
use old-footer missing
|
||
use cycle-test cycle
|
||
```
|
||
|
||
### 15.4 循环引用检测
|
||
|
||
必须检测:
|
||
|
||
```text
|
||
A 引用 B
|
||
B 引用 C
|
||
C 引用 A
|
||
```
|
||
|
||
渲染器或引用索引应报错:
|
||
|
||
```text
|
||
检测到循环片段引用:A → B → C → A
|
||
```
|
||
|
||
### 15.5 片段 props
|
||
|
||
v0.1 模型预留 props,UI 可先简单实现或后置。
|
||
|
||
```text
|
||
@use standard-title {
|
||
title = "角色设定"
|
||
description = "以下是角色基础信息。"
|
||
}
|
||
```
|
||
|
||
保存为:
|
||
|
||
```json
|
||
{
|
||
"type": "fragmentRef",
|
||
"fragmentId": "standard-title",
|
||
"props": {
|
||
"title": "角色设定",
|
||
"description": "以下是角色基础信息。"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 15.6 片段作用域规则
|
||
|
||
片段 props 是局部作用域,优先级高于外层数据。
|
||
|
||
变量查找顺序:
|
||
|
||
```text
|
||
循环局部变量
|
||
↓
|
||
片段 props
|
||
↓
|
||
模板局部输入
|
||
↓
|
||
全局数据集
|
||
↓
|
||
默认值 / fallback
|
||
```
|
||
|
||
未传入的变量继续向外层查找。
|
||
|
||
---
|
||
|
||
## 16. 表达式系统
|
||
|
||
### 16.1 表达式原则
|
||
|
||
第一版表达式系统必须安全、轻量、无副作用。不得执行任意 JavaScript。
|
||
|
||
表达式只允许:
|
||
|
||
- 读取数据
|
||
- 比较判断
|
||
- 简单逻辑计算
|
||
- 空值合并
|
||
- length 访问
|
||
- contains 判断
|
||
|
||
表达式不允许:
|
||
|
||
- 赋值
|
||
- 函数定义
|
||
- 任意 JS 执行
|
||
- 网络请求
|
||
- 文件读写
|
||
- 外部命令
|
||
- 异步调用
|
||
- 修改状态
|
||
|
||
### 16.2 v0.1 支持表达式
|
||
|
||
```text
|
||
user.name
|
||
user.level >= 3
|
||
user.type == "developer"
|
||
tags contains "important"
|
||
items.length > 0
|
||
!disabled
|
||
isVip && level >= 3
|
||
type == "code" || type == "doc"
|
||
user.name ?? "未命名"
|
||
```
|
||
|
||
### 16.3 支持能力表
|
||
|
||
```text
|
||
路径访问 user.name
|
||
数组访问 items[0].title
|
||
比较 == != > >= < <=
|
||
逻辑 && || !
|
||
包含 contains
|
||
空值合并 ??
|
||
长度 items.length
|
||
字符串字面量 "developer"
|
||
数字 123
|
||
布尔 true / false
|
||
null null
|
||
```
|
||
|
||
### 16.4 表达式模块建议
|
||
|
||
建议自研小型 parser/evaluator,或使用安全表达式库,但不得直接 `eval()`。
|
||
|
||
模块:
|
||
|
||
```text
|
||
expression/
|
||
tokenizer.ts
|
||
parser.ts
|
||
evaluator.ts
|
||
types.ts
|
||
```
|
||
|
||
---
|
||
|
||
## 17. 渲染模型
|
||
|
||
### 17.1 纯函数式渲染
|
||
|
||
渲染公式:
|
||
|
||
```text
|
||
BlockTree + DataSet + Fragments + Options => RenderResult
|
||
```
|
||
|
||
渲染结果不仅包含 output,还包含 logs、warnings、errors、stats。
|
||
|
||
### 17.2 渲染流程
|
||
|
||
```text
|
||
1. 加载入口模板
|
||
2. 创建根渲染上下文
|
||
3. 从根 Block 开始递归渲染
|
||
4. ParagraphBlock 渲染所有 InlineNode
|
||
5. VariableInline 从作用域读取值
|
||
6. ConditionBlock 判断表达式
|
||
7. LoopBlock 遍历数组并创建局部作用域
|
||
8. FragmentRefBlock 加载片段并创建片段作用域
|
||
9. SlotBlock 接收 fill 或 fallback
|
||
10. CommentBlock 忽略
|
||
11. OutputBlock 影响输出配置
|
||
12. 合并文本
|
||
13. 返回输出、日志、警告、错误、统计
|
||
```
|
||
|
||
### 17.3 核心函数建议
|
||
|
||
```ts
|
||
export function renderTemplate(input: RenderInput): RenderResult
|
||
|
||
function renderBlocks(blocks: BlockNode[], context: RenderContext): RenderChunk
|
||
|
||
function renderBlock(block: BlockNode, context: RenderContext): RenderChunk
|
||
|
||
function renderInlines(inlines: InlineNode[], context: RenderContext): string
|
||
|
||
function renderInline(inline: InlineNode, context: RenderContext): string
|
||
```
|
||
|
||
### 17.4 RenderContext 建议
|
||
|
||
```ts
|
||
type RenderContext = {
|
||
mode: "preview" | "export"
|
||
scopes: ScopeFrame[]
|
||
fragments: Record<string, FragmentDocument>
|
||
options: RenderOptions
|
||
logs: RenderLog[]
|
||
warnings: RenderWarning[]
|
||
errors: RenderError[]
|
||
stats: RenderStats
|
||
fragmentStack: string[]
|
||
}
|
||
|
||
type ScopeFrame = {
|
||
type: "data" | "template" | "fragment" | "loop" | "slot"
|
||
values: Record<string, unknown>
|
||
}
|
||
```
|
||
|
||
### 17.5 作用域解析
|
||
|
||
查找变量时,从内到外查找 scopes。
|
||
|
||
顺序:
|
||
|
||
```text
|
||
最近的 loop scope
|
||
fragment props scope
|
||
template scope
|
||
data scope
|
||
fallback
|
||
```
|
||
|
||
### 17.6 Paragraph 渲染
|
||
|
||
ParagraphBlock 渲染所有 inlines 并默认追加换行。具体策略可在 rendering 中控制。
|
||
|
||
建议 v0.1 默认:
|
||
|
||
```text
|
||
ParagraphBlock => renderInlines(...) + "\n"
|
||
ContainerBlock => 递归渲染 children,不额外增加换行
|
||
ConditionBlock => 命中时渲染 children,不命中且无 else 时不输出
|
||
LoopBlock => 渲染每个 item,使用 separator 或默认空字符串/换行策略
|
||
CommentBlock => 输出空字符串
|
||
```
|
||
|
||
换行策略必须尽早统一,避免条件隐藏后产生多余空行。
|
||
|
||
---
|
||
|
||
## 18. 缺失变量策略
|
||
|
||
### 18.1 全局规则
|
||
|
||
```text
|
||
预览:缺失变量保留占位符,并显示警告
|
||
导出:缺失变量默认报错,需要确认才能继续
|
||
变量级 fallback 优先于全局策略
|
||
```
|
||
|
||
### 18.2 预览示例
|
||
|
||
模板:
|
||
|
||
```text
|
||
你好,{user.name}
|
||
```
|
||
|
||
数据:
|
||
|
||
```json
|
||
{
|
||
"user": {}
|
||
}
|
||
```
|
||
|
||
预览输出:
|
||
|
||
```text
|
||
你好,{user.name}
|
||
```
|
||
|
||
调试面板:
|
||
|
||
```text
|
||
⚠ 缺失变量:user.name
|
||
```
|
||
|
||
### 18.3 导出示例
|
||
|
||
导出时若变量缺失,默认阻止导出:
|
||
|
||
```text
|
||
存在缺失变量:
|
||
- user.name
|
||
- order.id
|
||
|
||
是否继续导出?
|
||
```
|
||
|
||
操作:
|
||
|
||
```text
|
||
返回编辑
|
||
继续导出
|
||
复制带占位符结果
|
||
```
|
||
|
||
### 18.4 fallback
|
||
|
||
模板:
|
||
|
||
```text
|
||
你好,{user.name ?? "未命名"}
|
||
```
|
||
|
||
输出:
|
||
|
||
```text
|
||
你好,未命名
|
||
```
|
||
|
||
调试日志:
|
||
|
||
```text
|
||
user.name 缺失,已使用 fallback:"未命名"
|
||
```
|
||
|
||
---
|
||
|
||
## 19. 数据输入系统
|
||
|
||
### 19.1 双入口
|
||
|
||
数据输入采用:
|
||
|
||
```text
|
||
JSON 是高级主入口
|
||
表单是自动生成的辅助入口
|
||
```
|
||
|
||
### 19.2 JSON 数据面板
|
||
|
||
右侧 Data Tab 以 JSON 编辑为主。
|
||
|
||
```json
|
||
{
|
||
"user": {
|
||
"name": "张三",
|
||
"isVip": true
|
||
},
|
||
"items": [
|
||
{
|
||
"title": "产品 A",
|
||
"price": 99
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 19.3 自动表单
|
||
|
||
软件可根据变量与 InputSchema 生成表单。
|
||
|
||
```text
|
||
user.name [张三]
|
||
user.isVip [true]
|
||
items[0].title [产品 A]
|
||
```
|
||
|
||
v0.1 可先重点完成 JSON 模式,表单模式可简化。
|
||
|
||
### 19.4 数据集
|
||
|
||
一个模板可以保存多组测试数据:
|
||
|
||
```text
|
||
默认数据
|
||
空数据
|
||
VIP 用户
|
||
普通用户
|
||
极端长文本
|
||
多项目列表
|
||
```
|
||
|
||
数据集文件单独保存于 datasets 目录。
|
||
|
||
---
|
||
|
||
## 20. 输出系统
|
||
|
||
### 20.1 v0.1 输出范围
|
||
|
||
v0.1 只做单模板单输出。
|
||
|
||
支持:
|
||
|
||
```text
|
||
复制到剪贴板
|
||
导出 .txt
|
||
导出 .md
|
||
导出 .json
|
||
导出 .yaml
|
||
导出 .html
|
||
导出自定义扩展名文本
|
||
```
|
||
|
||
不做:
|
||
|
||
```text
|
||
批量导出
|
||
多文件生成
|
||
目录生成
|
||
```
|
||
|
||
### 20.2 OutputBlock
|
||
|
||
DSL:
|
||
|
||
```text
|
||
@output markdown
|
||
```
|
||
|
||
对应 OutputBlock。
|
||
|
||
右侧属性:
|
||
|
||
```text
|
||
输出格式
|
||
文件名模板
|
||
缺失变量策略
|
||
trimFinalOutput
|
||
ensureTrailingNewline
|
||
```
|
||
|
||
### 20.3 文件名模板
|
||
|
||
支持:
|
||
|
||
```text
|
||
{title}-{date}.md
|
||
```
|
||
|
||
使用当前数据集渲染得到文件名。
|
||
|
||
### 20.4 预览范围
|
||
|
||
右侧 Preview Tab 支持:
|
||
|
||
```text
|
||
完整模板
|
||
当前块
|
||
```
|
||
|
||
---
|
||
|
||
## 21. UI 设计
|
||
|
||
### 21.1 总体布局
|
||
|
||
三栏布局:
|
||
|
||
```text
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ 顶部工具栏:项目名 / 当前模板 / 保存状态 / 预览 / 导出 / 搜索 │
|
||
├───────────────┬───────────────────────────┬──────────────────┤
|
||
│ 左侧资源区 │ 中间 BlockFlow 编辑器 │ 右侧检查器 │
|
||
│ │ │ │
|
||
│ Templates │ ▾ 模板:main │ 属性 │
|
||
│ Fragments │ 你好,[user.name] │ 数据 │
|
||
│ Datasets │ │ 预览 │
|
||
│ Schemas │ ▾ if user.isVip │ 调试 │
|
||
│ Exports │ 感谢支持 │ 引用 │
|
||
├───────────────┴───────────────────────────┴──────────────────┤
|
||
│ 底部状态栏:已保存 / 缺失变量 2 / 引用错误 0 / 当前数据集 │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 21.2 视觉风格
|
||
|
||
v0.1 采用:
|
||
|
||
```text
|
||
深色优先
|
||
类 IDE
|
||
低饱和
|
||
细线框
|
||
高信息密度
|
||
低装饰
|
||
键盘友好
|
||
```
|
||
|
||
不要大面积卡片,不要花哨彩色块。
|
||
|
||
### 21.3 顶部工具栏
|
||
|
||
包含:
|
||
|
||
```text
|
||
项目名
|
||
当前打开文件
|
||
保存状态
|
||
当前数据集选择
|
||
复制输出
|
||
导出
|
||
全局搜索
|
||
命令面板
|
||
```
|
||
|
||
快捷键建议:
|
||
|
||
```text
|
||
Ctrl + P 快速打开资源
|
||
Ctrl + Shift + P 命令面板
|
||
Ctrl + S 手动保存
|
||
Ctrl + Z 撤销
|
||
Ctrl + Y 重做
|
||
```
|
||
|
||
### 21.4 左侧资源区
|
||
|
||
显示真实项目文件树和增强信息。
|
||
|
||
```text
|
||
Project: MyProject
|
||
|
||
Templates
|
||
main
|
||
prompt-generator
|
||
code-template
|
||
|
||
Fragments
|
||
common-footer 12 refs
|
||
output-rule 5 refs
|
||
role-block 2 refs
|
||
|
||
Datasets
|
||
default
|
||
empty
|
||
full-case
|
||
|
||
Schemas
|
||
common-inputs
|
||
|
||
Exports
|
||
latest-output.md
|
||
```
|
||
|
||
右键操作:
|
||
|
||
```text
|
||
新建
|
||
重命名
|
||
删除
|
||
复制
|
||
移动
|
||
在文件夹中显示
|
||
刷新
|
||
```
|
||
|
||
### 21.5 中间编辑器
|
||
|
||
示例视觉:
|
||
|
||
```text
|
||
▾ # 用户通知模板
|
||
|
||
你好,[user.name]
|
||
|
||
▾ if user.isVip
|
||
感谢你作为 VIP 用户长期支持我们。
|
||
|
||
▾ for item in items
|
||
- [item.title]: [item.price]
|
||
|
||
use common-footer
|
||
```
|
||
|
||
变量胶囊样式应低调:
|
||
|
||
```text
|
||
[user.name]
|
||
[user.email !]
|
||
[user.age ?]
|
||
```
|
||
|
||
容器应像代码折叠区:
|
||
|
||
```text
|
||
▾ if user.isVip
|
||
...
|
||
```
|
||
|
||
而不是巨大卡片。
|
||
|
||
### 21.6 右侧检查器
|
||
|
||
Tab:
|
||
|
||
```text
|
||
属性 | 数据 | 预览 | 调试 | 引用
|
||
```
|
||
|
||
#### 属性 Tab
|
||
|
||
选中变量:
|
||
|
||
```text
|
||
Variable
|
||
Path user.name
|
||
Fallback 未命名
|
||
Required true
|
||
```
|
||
|
||
选中条件:
|
||
|
||
```text
|
||
Condition
|
||
Expression user.isVip
|
||
Current true
|
||
```
|
||
|
||
选中循环:
|
||
|
||
```text
|
||
Loop
|
||
Source items
|
||
Item item
|
||
Index index
|
||
Separator newline
|
||
```
|
||
|
||
选中片段:
|
||
|
||
```text
|
||
Fragment Ref
|
||
Fragment common-footer
|
||
Status found
|
||
Actions Open / Expand
|
||
```
|
||
|
||
#### 数据 Tab
|
||
|
||
```text
|
||
数据集:default [切换] [新建] [复制]
|
||
模式:JSON | Form
|
||
```
|
||
|
||
#### 预览 Tab
|
||
|
||
```text
|
||
完整模板 | 当前块
|
||
复制 | 导出 | 重新渲染
|
||
```
|
||
|
||
#### 调试 Tab
|
||
|
||
```text
|
||
✓ variable user.name = "张三"
|
||
✓ condition user.isVip => true
|
||
✓ loop items => 2 items
|
||
✓ fragment common-footer loaded
|
||
⚠ variable user.email missing
|
||
```
|
||
|
||
#### 引用 Tab
|
||
|
||
```text
|
||
当前文件:fragments/common-footer.json
|
||
|
||
被引用于:
|
||
- templates/main.json
|
||
- templates/prompt-generator.json
|
||
|
||
引用了:
|
||
- fragments/signature.json
|
||
|
||
状态:
|
||
✓ 无缺失引用
|
||
✓ 无循环引用
|
||
```
|
||
|
||
### 21.7 底部状态栏
|
||
|
||
```text
|
||
已保存 | default dataset | missing vars: 1 | refs: ok | main.json
|
||
```
|
||
|
||
错误状态:
|
||
|
||
```text
|
||
保存失败 | 缺失变量 2 | 缺失片段 1 | 外部文件已修改
|
||
```
|
||
|
||
---
|
||
|
||
## 22. 资源管理系统
|
||
|
||
### 22.1 真实文件系统优先
|
||
|
||
项目资源以真实文件夹为准。软件左侧资源区显示文件树,并提供搜索、标签、引用状态等增强信息。
|
||
|
||
### 22.2 索引只是缓存
|
||
|
||
软件增强索引负责:
|
||
|
||
```text
|
||
搜索模板
|
||
搜索片段
|
||
搜索变量
|
||
搜索文本内容
|
||
显示标签
|
||
显示引用状态
|
||
显示缺失片段
|
||
显示循环引用
|
||
显示最近使用
|
||
```
|
||
|
||
但 JSON 文件是真相,索引不是主数据。
|
||
|
||
### 22.3 标签系统
|
||
|
||
标签写在模板/片段/数据集 JSON 中。
|
||
|
||
```json
|
||
{
|
||
"id": "fragment_output_rule",
|
||
"name": "输出格式约束",
|
||
"tags": ["prompt", "format", "rule"],
|
||
"children": []
|
||
}
|
||
```
|
||
|
||
目录负责物理组织,标签负责逻辑组织。
|
||
|
||
---
|
||
|
||
## 23. 引用追踪
|
||
|
||
### 23.1 v0.1 只做引用追踪面板
|
||
|
||
不做大型依赖图。先解决实际维护问题:
|
||
|
||
```text
|
||
谁引用了我?
|
||
我引用了谁?
|
||
有没有缺失片段?
|
||
有没有循环引用?
|
||
```
|
||
|
||
### 23.2 索引构建
|
||
|
||
扫描所有 TemplateDocument 和 FragmentDocument,找出 FragmentRefBlock 和 InlineFragmentRef。
|
||
|
||
建立:
|
||
|
||
```text
|
||
outgoing: 当前文档引用了哪些片段
|
||
incoming: 当前片段被哪些文档引用
|
||
missingFragments: 缺失片段引用
|
||
cycles: 循环引用链
|
||
```
|
||
|
||
### 23.3 循环检测
|
||
|
||
可使用 DFS 检测有向图环。
|
||
|
||
示例:
|
||
|
||
```text
|
||
A → B → C → A
|
||
```
|
||
|
||
输出 ReferenceCycle。
|
||
|
||
---
|
||
|
||
## 24. 自动保存与外部修改检测
|
||
|
||
### 24.1 自动保存
|
||
|
||
默认自动保存。
|
||
|
||
建议策略:
|
||
|
||
```text
|
||
停止编辑 800ms 后自动保存
|
||
保存前做 JSON 校验
|
||
保存失败时提示
|
||
保留最近一次成功保存状态
|
||
```
|
||
|
||
状态显示:
|
||
|
||
```text
|
||
已保存
|
||
保存中...
|
||
保存失败:templates/main.json 无法写入
|
||
```
|
||
|
||
### 24.2 撤销重做
|
||
|
||
必须支持:
|
||
|
||
```text
|
||
Ctrl + Z
|
||
Ctrl + Y / Ctrl + Shift + Z
|
||
```
|
||
|
||
包括:
|
||
|
||
```text
|
||
输入文本
|
||
插入变量
|
||
创建容器
|
||
拖拽块
|
||
修改属性
|
||
包裹选区
|
||
展开片段
|
||
```
|
||
|
||
### 24.3 外部修改检测
|
||
|
||
软件应监听项目文件夹变化。
|
||
|
||
如果当前文件在外部被修改:
|
||
|
||
- 当前无未保存内容:提示重新加载/忽略
|
||
- 当前有未保存内容:进入 conflict 状态
|
||
|
||
冲突操作:
|
||
|
||
```text
|
||
使用外部版本
|
||
使用当前版本覆盖
|
||
另存为副本
|
||
查看差异
|
||
```
|
||
|
||
v0.1 不做自动合并。
|
||
|
||
---
|
||
|
||
## 25. Editor Adapter 设计
|
||
|
||
### 25.1 核心原则
|
||
|
||
TipTap/ProseMirror 只做编辑视图层。项目文件只保存 BlockFlow AST。
|
||
|
||
需要双向转换:
|
||
|
||
```text
|
||
BlockFlow AST → TipTap Doc
|
||
TipTap Doc → BlockFlow AST
|
||
```
|
||
|
||
### 25.2 AST 到 TipTap
|
||
|
||
示例:
|
||
|
||
- ParagraphBlock → paragraph node
|
||
- TextInline → text
|
||
- VariableInline → custom inline atom node
|
||
- ContainerBlock → custom block node with content
|
||
- ConditionBlock → custom block node with attrs.expression
|
||
- LoopBlock → custom block node with attrs.source / itemName / indexName
|
||
- FragmentRefBlock → custom block atom or block node
|
||
|
||
### 25.3 TipTap 到 AST
|
||
|
||
保存时将当前 editor doc 转换回 BlockFlow AST。不要直接保存 TipTap JSON。
|
||
|
||
### 25.4 节点扩展
|
||
|
||
v0.1 至少需要:
|
||
|
||
```text
|
||
VariableInlineExtension
|
||
ContainerBlockExtension
|
||
ConditionBlockExtension
|
||
LoopBlockExtension
|
||
FragmentRefExtension
|
||
CommentBlockExtension
|
||
```
|
||
|
||
---
|
||
|
||
## 26. 命令菜单
|
||
|
||
### 26.1 `/` 菜单 v0.1 项
|
||
|
||
```text
|
||
/变量
|
||
/容器
|
||
/条件
|
||
/循环
|
||
/片段引用
|
||
/插槽
|
||
/注释
|
||
/输出
|
||
```
|
||
|
||
### 26.2 模糊搜索
|
||
|
||
支持中文和英文:
|
||
|
||
```text
|
||
/if
|
||
/条件
|
||
/var
|
||
/变量
|
||
/loop
|
||
/循环
|
||
```
|
||
|
||
### 26.3 创建后默认行为
|
||
|
||
创建变量:立即聚焦变量名。
|
||
创建条件:自动选中条件表达式。
|
||
创建循环:自动选中 source。
|
||
创建容器:光标进入容器内容。
|
||
创建片段引用:弹出片段选择器。
|
||
|
||
---
|
||
|
||
## 27. 快捷键建议
|
||
|
||
v0.1 可内置以下快捷键,后续再支持自定义:
|
||
|
||
```text
|
||
Ctrl + P 快速打开模板/片段/数据集
|
||
Ctrl + Shift + P 命令面板
|
||
Ctrl + S 手动保存
|
||
Ctrl + Z 撤销
|
||
Ctrl + Y 重做
|
||
Ctrl + Shift + Z 重做
|
||
Ctrl + / 转为注释
|
||
Ctrl + Alt + G 包裹为容器
|
||
Ctrl + Alt + I 包裹为条件
|
||
Ctrl + Alt + L 包裹为循环
|
||
Ctrl + Alt + F 保存为片段
|
||
```
|
||
|
||
---
|
||
|
||
## 28. 开发里程碑
|
||
|
||
### Milestone 1:核心 AST + 渲染器
|
||
|
||
先不做 UI,直接用 JSON 测试。
|
||
|
||
目标:
|
||
|
||
```text
|
||
手写 template.json
|
||
手写 dataset.json
|
||
运行 renderer
|
||
得到 output string + logs + errors
|
||
```
|
||
|
||
支持:
|
||
|
||
```text
|
||
paragraph
|
||
text inline
|
||
variable inline
|
||
condition
|
||
loop
|
||
container
|
||
fragment
|
||
comment
|
||
```
|
||
|
||
完成标准:
|
||
|
||
```text
|
||
变量能替换
|
||
条件能判断
|
||
循环能展开
|
||
片段能引用
|
||
缺失变量能产生 warning/error
|
||
循环引用能检测
|
||
```
|
||
|
||
### Milestone 2:项目文件夹系统
|
||
|
||
目标:
|
||
|
||
```text
|
||
打开项目文件夹
|
||
读取 project.json
|
||
扫描 templates/
|
||
扫描 fragments/
|
||
扫描 datasets/
|
||
构建内存索引
|
||
```
|
||
|
||
完成标准:
|
||
|
||
```text
|
||
左侧能看到真实文件树
|
||
能打开模板
|
||
能切换数据集
|
||
能保存 JSON
|
||
能检测缺失片段
|
||
```
|
||
|
||
### Milestone 3:最小三栏 UI
|
||
|
||
先做壳子:
|
||
|
||
```text
|
||
顶部工具栏
|
||
左侧项目资源区
|
||
中间占位编辑区
|
||
右侧属性/数据/预览/调试/引用
|
||
底部状态栏
|
||
```
|
||
|
||
### Milestone 4:TipTap 编辑器接入
|
||
|
||
第一批节点:
|
||
|
||
```text
|
||
ParagraphBlock
|
||
VariableInline
|
||
ContainerBlock
|
||
ConditionBlock
|
||
LoopBlock
|
||
FragmentBlock
|
||
CommentBlock
|
||
```
|
||
|
||
第一批输入规则:
|
||
|
||
```text
|
||
{user.name} 自动变变量胶囊
|
||
@if user.isVip 回车变条件块
|
||
@for item in items 回车变循环块
|
||
/ 打开命令菜单
|
||
```
|
||
|
||
完成标准:
|
||
|
||
```text
|
||
能写普通文本
|
||
能插入变量
|
||
能插入条件
|
||
能插入循环
|
||
能保存为 BlockFlow AST
|
||
能重新打开不丢结构
|
||
```
|
||
|
||
### Milestone 5:右侧数据与实时预览
|
||
|
||
目标:
|
||
|
||
```text
|
||
右侧 JSON 数据编辑
|
||
实时渲染
|
||
显示完整模板预览
|
||
显示当前块预览
|
||
显示缺失变量
|
||
显示渲染日志
|
||
```
|
||
|
||
### Milestone 6:片段系统与引用追踪
|
||
|
||
目标:
|
||
|
||
```text
|
||
创建片段
|
||
引用片段
|
||
打开片段
|
||
展开片段
|
||
检测缺失片段
|
||
检测循环引用
|
||
显示被哪些文件引用
|
||
```
|
||
|
||
### Milestone 7:选区转块与片段化
|
||
|
||
目标:
|
||
|
||
```text
|
||
选区包裹为容器
|
||
选区包裹为条件
|
||
选区包裹为循环
|
||
选区保存为片段
|
||
选区转为注释
|
||
```
|
||
|
||
### Milestone 8:导出与源码模式
|
||
|
||
目标:
|
||
|
||
```text
|
||
复制最终输出
|
||
导出 txt / md / json / yaml / 任意扩展名
|
||
AST → DSL
|
||
DSL → AST
|
||
全文源码模式
|
||
局部源码编辑
|
||
```
|
||
|
||
---
|
||
|
||
## 29. 验收标准
|
||
|
||
v0.1 可以被认为可用时,应满足:
|
||
|
||
1. 可以创建或打开一个项目文件夹。
|
||
2. 可以在左侧资源树看到模板、片段、数据集。
|
||
3. 可以打开模板并编辑普通文本。
|
||
4. 输入 `{user.name}` 可以生成变量胶囊。
|
||
5. 输入 `@if user.isVip` 回车可以生成条件块。
|
||
6. 输入 `@for item in items` 回车可以生成循环块。
|
||
7. 可以通过 `/` 菜单插入基础块。
|
||
8. 可以输入或编辑 JSON 数据集。
|
||
9. 预览面板能实时显示渲染结果。
|
||
10. 缺失变量在预览时保留占位符并显示警告。
|
||
11. 导出时缺失变量默认报错。
|
||
12. 片段引用能正常渲染。
|
||
13. 缺失片段能被提示。
|
||
14. 循环片段引用能被检测。
|
||
15. 可以复制最终输出。
|
||
16. 可以导出单个文件。
|
||
17. 项目内容保存为 JSON AST,而不是 TipTap JSON。
|
||
18. 自动保存可用。
|
||
19. 外部修改检测可用。
|
||
20. 关闭重开项目后结构不丢失。
|
||
|
||
---
|
||
|
||
## 30. 测试用例建议
|
||
|
||
### 30.1 基础变量
|
||
|
||
模板:
|
||
|
||
```text
|
||
你好,{user.name}
|
||
```
|
||
|
||
数据:
|
||
|
||
```json
|
||
{
|
||
"user": {
|
||
"name": "张三"
|
||
}
|
||
}
|
||
```
|
||
|
||
期望输出:
|
||
|
||
```text
|
||
你好,张三
|
||
```
|
||
|
||
### 30.2 缺失变量预览
|
||
|
||
模板:
|
||
|
||
```text
|
||
你好,{user.name}
|
||
```
|
||
|
||
数据:
|
||
|
||
```json
|
||
{
|
||
"user": {}
|
||
}
|
||
```
|
||
|
||
预览输出:
|
||
|
||
```text
|
||
你好,{user.name}
|
||
```
|
||
|
||
warnings 包含 MISSING_VARIABLE。
|
||
|
||
### 30.3 fallback
|
||
|
||
模板:
|
||
|
||
```text
|
||
你好,{user.name ?? "未命名"}
|
||
```
|
||
|
||
数据:
|
||
|
||
```json
|
||
{
|
||
"user": {}
|
||
}
|
||
```
|
||
|
||
输出:
|
||
|
||
```text
|
||
你好,未命名
|
||
```
|
||
|
||
### 30.4 条件命中
|
||
|
||
模板:
|
||
|
||
```text
|
||
@if user.isVip
|
||
VIP 用户
|
||
@endif
|
||
```
|
||
|
||
数据:
|
||
|
||
```json
|
||
{
|
||
"user": {
|
||
"isVip": true
|
||
}
|
||
}
|
||
```
|
||
|
||
输出包含:
|
||
|
||
```text
|
||
VIP 用户
|
||
```
|
||
|
||
### 30.5 条件未命中
|
||
|
||
数据:
|
||
|
||
```json
|
||
{
|
||
"user": {
|
||
"isVip": false
|
||
}
|
||
}
|
||
```
|
||
|
||
输出不包含条件内容。
|
||
|
||
### 30.6 循环
|
||
|
||
模板:
|
||
|
||
```text
|
||
@for item in items
|
||
- {item.title}: {item.price}
|
||
@endfor
|
||
```
|
||
|
||
数据:
|
||
|
||
```json
|
||
{
|
||
"items": [
|
||
{ "title": "A", "price": 1 },
|
||
{ "title": "B", "price": 2 }
|
||
]
|
||
}
|
||
```
|
||
|
||
输出:
|
||
|
||
```text
|
||
- A: 1
|
||
- B: 2
|
||
```
|
||
|
||
### 30.7 循环源不是数组
|
||
|
||
如果 `items` 不是数组,导出模式应产生 LOOP_SOURCE_NOT_ARRAY 错误。
|
||
|
||
### 30.8 片段引用
|
||
|
||
模板引用 `common-footer`,片段存在时正常渲染。
|
||
|
||
### 30.9 缺失片段
|
||
|
||
模板引用不存在片段时,产生 MISSING_FRAGMENT 错误。
|
||
|
||
### 30.10 循环片段引用
|
||
|
||
A 引用 B,B 引用 A,应产生 FRAGMENT_CYCLE 错误。
|
||
|
||
---
|
||
|
||
## 31. Codex 开发注意事项
|
||
|
||
### 31.1 不要偏离产品定位
|
||
|
||
不要把产品实现成:
|
||
|
||
- 普通 Markdown 编辑器
|
||
- 纯富文本编辑器
|
||
- 传统低代码平台
|
||
- AI 工具
|
||
- 数据库驱动工具
|
||
- 单文件封闭项目格式
|
||
|
||
### 31.2 不要让 TipTap 成为主数据模型
|
||
|
||
TipTap 只是编辑器视图层。保存文件时必须转回 BlockFlow AST。
|
||
|
||
### 31.3 不要使用 eval
|
||
|
||
表达式系统不得使用 `eval()` 或 `new Function()` 执行用户输入。
|
||
|
||
### 31.4 不要在 v0.1 引入复杂依赖
|
||
|
||
v0.1 应先完成核心闭环。不要过早引入 SQLite、插件系统、依赖图、批量导出等。
|
||
|
||
### 31.5 保持模块纯净
|
||
|
||
Renderer、Expression、Reference Index、Project IO 应尽量与 React UI 解耦,方便测试。
|
||
|
||
### 31.6 优先写单元测试
|
||
|
||
核心模块应优先测试:
|
||
|
||
- renderTemplate
|
||
- renderBlock
|
||
- renderInline
|
||
- expression evaluator
|
||
- missing variable handling
|
||
- fragment cycle detection
|
||
- AST <-> DSL
|
||
- AST <-> TipTap Doc
|
||
|
||
### 31.7 先实现核心闭环
|
||
|
||
开发顺序优先:
|
||
|
||
```text
|
||
类型定义
|
||
渲染器
|
||
表达式求值
|
||
项目文件夹读取
|
||
最小 UI
|
||
编辑器接入
|
||
实时预览
|
||
片段引用
|
||
导出
|
||
```
|
||
|
||
不要先花太多时间在视觉细节、复杂动画或非核心扩展上。
|
||
|
||
---
|
||
|
||
## 32. 一份完整模板 JSON 示例
|
||
|
||
```json
|
||
{
|
||
"kind": "template",
|
||
"id": "template_user_notice",
|
||
"name": "用户通知",
|
||
"tags": ["demo", "notice"],
|
||
"children": [
|
||
{
|
||
"id": "block_1",
|
||
"type": "container",
|
||
"name": "用户通知",
|
||
"children": [
|
||
{
|
||
"id": "block_2",
|
||
"type": "paragraph",
|
||
"inlines": [
|
||
{
|
||
"type": "text",
|
||
"content": "你好,"
|
||
},
|
||
{
|
||
"type": "variable",
|
||
"path": "user.name",
|
||
"fallback": "未命名"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "block_3",
|
||
"type": "condition",
|
||
"expression": "user.isVip",
|
||
"children": [
|
||
{
|
||
"id": "block_4",
|
||
"type": "paragraph",
|
||
"inlines": [
|
||
{
|
||
"type": "text",
|
||
"content": "感谢你作为 VIP 用户长期支持我们。"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "block_5",
|
||
"type": "loop",
|
||
"source": "items",
|
||
"itemName": "item",
|
||
"indexName": "index",
|
||
"children": [
|
||
{
|
||
"id": "block_6",
|
||
"type": "paragraph",
|
||
"inlines": [
|
||
{
|
||
"type": "text",
|
||
"content": "- "
|
||
},
|
||
{
|
||
"type": "variable",
|
||
"path": "item.title"
|
||
},
|
||
{
|
||
"type": "text",
|
||
"content": ": "
|
||
},
|
||
{
|
||
"type": "variable",
|
||
"path": "item.price"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "block_7",
|
||
"type": "fragmentRef",
|
||
"fragmentId": "fragment_common_footer"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 33. 一份完整数据集 JSON 示例
|
||
|
||
```json
|
||
{
|
||
"kind": "dataset",
|
||
"id": "dataset_default",
|
||
"name": "默认数据",
|
||
"tags": ["demo"],
|
||
"data": {
|
||
"user": {
|
||
"name": "张三",
|
||
"isVip": true
|
||
},
|
||
"items": [
|
||
{
|
||
"title": "产品 A",
|
||
"price": 99
|
||
},
|
||
{
|
||
"title": "产品 B",
|
||
"price": 199
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 34. 一份片段 JSON 示例
|
||
|
||
```json
|
||
{
|
||
"kind": "fragment",
|
||
"id": "fragment_common_footer",
|
||
"name": "通用结尾",
|
||
"tags": ["common", "footer"],
|
||
"children": [
|
||
{
|
||
"id": "footer_p1",
|
||
"type": "paragraph",
|
||
"inlines": [
|
||
{
|
||
"type": "text",
|
||
"content": "以上。"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 35. v0.1 最终定义
|
||
|
||
v0.1 的最终形态应是:
|
||
|
||
> 一个可以打开本地项目文件夹、编辑结构化文本模板、插入变量/条件/循环/片段、输入 JSON 数据、实时预览、查看调试日志、追踪片段引用并复制或导出单文件结果的桌面端高级文本结构编排工具。
|
||
|
||
它的核心闭环是:
|
||
|
||
```text
|
||
创建项目
|
||
↓
|
||
创建模板
|
||
↓
|
||
输入文本
|
||
↓
|
||
输入特殊符号转换为块
|
||
↓
|
||
嵌套组织容器
|
||
↓
|
||
引用片段
|
||
↓
|
||
输入 JSON 数据
|
||
↓
|
||
实时预览
|
||
↓
|
||
调试错误
|
||
↓
|
||
复制/导出结果
|
||
```
|
||
|
||
只要这个闭环足够顺畅,v0.1 就是成功的。
|
||
|
||
---
|
||
|
||
## 36. 后续版本方向
|
||
|
||
### v0.2
|
||
|
||
- 完整片段 props UI
|
||
- 完整插槽系统
|
||
- 自动表单生成增强
|
||
- DSL 源码模式增强
|
||
- 快捷键自定义
|
||
- 搜索正文与变量
|
||
- 更好的文件 diff
|
||
|
||
### v0.3
|
||
|
||
- 批量导出
|
||
- 多文件生成
|
||
- 依赖图谱
|
||
- SQLite 缓存索引
|
||
- 主题系统
|
||
- 模板包导入导出
|
||
|
||
### v1.0
|
||
|
||
- 稳定项目格式
|
||
- 完整文档
|
||
- 插件 API 初版
|
||
- 大型项目性能优化
|
||
- 可靠测试覆盖
|
||
|
||
---
|
||
|
||
## 37. 最重要的开发准则
|
||
|
||
开发时永远优先保护以下原则:
|
||
|
||
1. BlockFlow AST 是唯一真相。
|
||
2. 渲染器必须纯函数式、无副作用。
|
||
3. 项目文件夹是真实项目本体。
|
||
4. 可视化块服务于文本编辑效率。
|
||
5. 变量、条件、循环、片段是核心,不要被 UI 花活稀释。
|
||
6. v0.1 先做单模板单输出闭环,不要扩张到低代码平台。
|
||
7. 高信息密度、键盘友好、类 IDE 是产品气质。
|
||
8. 所有复杂功能都应围绕“复杂文本结构更容易维护”这个目标。
|
||
|