CYBERSWAT / 开发部自由练习 · v0.4.1

U06 · 保存的是原文,预览只是另一种显示

v0.4 · 先修:手册 16–22;工作台。选题参考:Markdown Note-taking App。采用原创需求,不搬运原题。

你要保存一段虚构学习笔记,并在旁边预览。先弄清两份东西:Markdown 原文是资料,HTML 是从它派生的显示。编辑时应取回原文,不能把预览 HTML 再当 Markdown 保存。

动手路线:每一步都有文件和结果

先完成 统一启动与恢复手册 的首次准备。以下命令均在自己的练习根目录执行;原文中的 api/web/desktop 概念对应本节列出的现成文件,不需要猜测如何搭空项目。

  1. 打开 u06/starter/model.ts,按 validateNote → renderPreview 的顺序逐个实现。下面第1节起解释规则;每完成一个函数就重跑样例/测试。
  2. 执行以下三条,分别核对样例、行为和类型。未完成的 TODO 应失败,全部实现后测试应为 fail 0:
node tools/try.mjs u06
node tools/check.mjs u06
npm run check
  1. 打开 runtime/src/u06.ts、runtime/web/u06.js,找到对自己规则函数的调用。先用一句话记录输入从哪里来、结果写到哪里;读不懂的语法回到本节先修对应章节。

  2. 先运行 node tools/lab.mjs db up,等待迁移完成。构建并启动自己的作品:

node tools/build-app.mjs u06
node tools/lab.mjs start u06
  1. 实际操作:输入 # 标题 和下一行 <script>bad()</script>;预览显示文字,不执行脚本。保存后刷新仍在;导出 JSON 再导入,非法条目不能造成半批写入。

在第二个终端运行 node tools/probe.mjs u06。脚本会添加合成数据并报告每组检查;不清空自己的数据。 首次通过后停止并重新启动服务,再运行 node tools/probe.mjs u06 --resume;数据库单元同时按统一手册重启数据库。

  1. 解释与再做一次:预览 HTML 在哪一步转义?为什么导入先全量验证再事务写入? 在自己的 LEARNING.md 先写答案,再选本节末尾的变式,列一个新失败例子后修改。保持原用例通过。仅跑通配套界面、没有完成规则与解释,记录为“参考演示已观察”,不记录为自己完成作品。

卡在规则时按“函数输入→分支条件→返回值→参考实现”逐级看提示;卡在运行时按 故障表 定位。若想单独观察参考成品,在构建命令后加 --reference;页面会明确标出参考演示。观察完再去掉该参数重建,继续自己的 starter。

1. 先限制语法,观察转换

打开 u06/starter/model.ts。第一版只支持行首 # 的一级标题,其余每行按普通段落显示;不支持链接、图片、代码围栏或完整 CommonMark。这个小范围让你能解释每一条转换,不把简化规则冒称完整 Markdown 引擎。

完成 validateNote:标题去空白且 1–60 个 UTF-16 码元,正文非全空白且不超过 4000 个码元,保留正文原始换行。完成 renderPreview:先把特殊 HTML 字符转义,再套固定 h1/p;Windows 的 CRLF 与 LF 都正确拆行。

运行样例、类型检查和 node tools/check.mjs u06。观察 <img ...> 应变成可见文字,不成为实际图片元素。

2. 先在页面证明编辑与预览分开

打开配套 runtime/web/u06.js 的 Vue 页面,title/body 是输入状态,预览由 body 计算,不维护第二份可独立编辑的 HTML。普通正文可以用模板文字绑定;如果将本节 renderer 生成结果用于 HTML 区,必须保证所有外来内容只经这条转义路径进入,不拼接未经处理的标题或文件名。

输入一级标题和两行正文,预览应分别显示。输入 HTML 字样,观察页面源结构,不能实际创建该标签。只看“没弹窗”不足以证明原文没有被当 HTML 插入。

3. 保存到自己的笔记资源

打开配套 Prisma Note:id、owner、title、body、updatedAt;这里 owner 对应手册 ownerId 的含义,更新时不接受客户端修改。契约为创建 201、本人读取与修改 200、非法输入 400、无身份 401、他人操作统一隐藏存在性为404。

逐个追踪已有 POST /api/notes、GET /api/notes/:id、PATCH /api/notes/:id 和列表,自己完成模型后分别运行验收。数据库只保存原文;预览从原文重新生成。直接接口传入非法 body,必须被服务端拒绝。

4. 导入导出前先处理失败

用浏览器文件选择读取自己选定的 .md 文件,先检查文件大小(本练习最多 64 KiB),读取为文本后再按正文 4000 码元限制检查;两种限制单位不同。文件名只建议标题,不直接变成服务器路径。校验失败保留现有输入,不覆盖正在编辑的笔记。

导出用 UTF-8 文本 Blob,内容必须是原 Markdown,不是预览 HTML。再把导出文件重新导入,比较原文、中文和换行。取消文件选择不等于导入空文本。

5. 增加一次完整失败观察

编辑一段尚未保存的文字,停掉本机 API,再点保存。应提示失败,并保留编辑内容;重新启动后可以再保存。不要在发请求前清空输入,也不要只凭按钮动画写“保存成功”。

6. 完成检查与变式

验证正常保存、空标题、超长正文、HTML 字样、导入失败保留原文、导出回读、双身份隔离、重启持久化。记录核心测试与真实页面/API 结果,二者不能互相替代。

独立变式:增加“未保存更改”提示,成功保存才清除;API 失败、取消导入都不应错误清除。完整 Markdown 库留作后续扩展:选定版本、记录许可,并重新核对 HTML/链接处理,不直接将本节测试当作完整渲染器安全证明。

暂停前保存文件和 README 中的启动位置;不用外部语法检查或 AI 服务也能完成本节。下一步 U11 比较另一种存储方式。