把正文组成收成数据契约,程序投影与旁观维护不再靠模型临场猜。

有变量真值时固定开旁观填表,开场与游玩只填模型生成区。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-07 23:47:58 +08:00
co-authored by Cursor
parent bb4f934b3d
commit d0a48f9431
21 changed files with 3094 additions and 224 deletions
+148 -1
View File
@@ -4,6 +4,7 @@ import {
PRESENT_FRAME_SANDBOX,
buildPresentSrcdoc,
isFrontendBundle,
presentFrameSlotHtml,
presentSourceBytes,
previewPacket,
} from "../web/present-frame.js";
@@ -24,7 +25,8 @@ describe("present frame srcdoc", () => {
});
expect(srcdoc).toContain(`<div id="app"></div>`);
expect(srcdoc).toContain(`#app { color: #e1e7f0; }`);
expect(srcdoc).toContain("present.onData");
expect(srcdoc).toContain("present-host");
expect(srcdoc).toContain("applyPacket");
expect(srcdoc).toContain("示例正文");
expect(srcdoc).toContain(`default-src 'none'`);
expect(srcdoc.indexOf("window.present")).toBeLessThan(srcdoc.indexOf("data.blocks.body"));
@@ -54,6 +56,112 @@ describe("present frame srcdoc", () => {
expect(PRESENT_BYTE_LIMIT).toBe(60 * 1024);
});
it("sizes the frame to content and fills contract slots", () => {
const srcdoc = buildPresentSrcdoc({
html: `<article data-block="body"></article>`,
css: `html,body{height:100vh;overflow:auto}`,
js: `present.onData(function () {});`,
packet: { schema: "present.v1", blocks: { body: "正文", forum_feed: ["一帖"] } },
slots: [
{ id: "body", type: "markdown" },
{ id: "forum_feed", type: "list" },
],
frameId: "pf3",
});
expect(srcdoc).not.toContain("100vh");
expect(srcdoc).not.toContain("overflow:auto");
expect(srcdoc).toContain("overflow:hidden!important");
expect(srcdoc).toContain("fillSlots");
expect(srcdoc).toContain("isProgramOwned");
expect(srcdoc).toContain("forum_feed");
expect(srcdoc).toContain('"type":"list"');
});
it("turns markdown newlines into paragraphs even if the shell wrote textContent first", () => {
const srcdoc = buildPresentSrcdoc({
html: `<article id="body" data-block="body"></article>`,
css: "",
js: `present.onData(function (data) {
document.getElementById("body").textContent = data.blocks.body;
});`,
packet: {
schema: "present.v1",
blocks: { body: "第一段\\n\\n第二段 **加粗**" },
},
slots: [{ id: "body", type: "markdown" }],
frameId: "pf4",
});
// packet JSON keeps real newlines; template sample above used escaped for the test string —
// rebuild with actual newlines:
const withBreaks = buildPresentSrcdoc({
html: `<article id="body" data-block="body"></article>`,
css: "",
js: `present.onData(function (data) {
document.getElementById("body").textContent = data.blocks.body;
});`,
packet: {
schema: "present.v1",
blocks: { body: "第一段\n\n第二段 **加粗**" },
},
slots: [{ id: "body", type: "markdown" }],
frameId: "pf4b",
});
const match = withBreaks.match(/function markdown\(raw\) \{[\s\S]*?\n \}/);
expect(match).toBeTruthy();
const markdown = new Function(`${match![0]}; return markdown;`)();
const html = markdown("第一段\n\n第二段 **加粗**");
expect(html).toContain("<p>第一段</p>");
expect(html).toContain("<p>第二段 <strong>加粗</strong></p>");
expect(withBreaks).toContain("isProgramOwned");
expect(srcdoc).toContain("isProgramOwned");
});
it("applies program fill after shell onData so textContent cannot keep a wall of text", () => {
const srcdoc = buildPresentSrcdoc({
html: `<article id="body" data-block="body"></article>`,
css: "",
js: `present.onData(function (data) {
document.getElementById("body").textContent = data.blocks.body;
});`,
packet: { schema: "present.v1", blocks: { body: "a\\nb" } },
slots: [{ id: "body", type: "markdown" }],
frameId: "pf5",
});
const sealIdx = srcdoc.indexOf("present.__seal");
const ownedIdx = srcdoc.indexOf("isProgramOwned");
expect(ownedIdx).toBeGreaterThan(-1);
expect(sealIdx).toBeGreaterThan(-1);
// applyPacket: renderCb then fillSlots
const applyIdx = srcdoc.indexOf("function applyPacket");
const applyChunk = srcdoc.slice(applyIdx, applyIdx + 450);
expect(applyChunk.indexOf("renderCb")).toBeLessThan(applyChunk.lastIndexOf("fillSlots"));
expect(srcdoc).toContain('t === "kv" || t === "table" || t === "topology"');
});
it("renders list objects by text/label instead of JSON.stringify", () => {
const srcdoc = buildPresentSrcdoc({
html: `<ol data-block="options"></ol>`,
css: "",
js: `present.onData(function () {});`,
packet: { schema: "present.v1", blocks: { options: [] } },
slots: [{ id: "options", type: "list" }],
frameId: "pf6",
});
const match = srcdoc.match(/function listItemText\(item\) \{[\s\S]*?\n \}/);
expect(match).toBeTruthy();
const listItemText = new Function(`${match![0]}; return listItemText;`)();
expect(listItemText({ type: "继续推进1", text: "顺着旧话试探" })).toBe("顺着旧话试探");
expect(listItemText({ label: "换方向" })).toBe("换方向");
expect(listItemText("纯字符串")).toBe("纯字符串");
expect(listItemText('{"type":"x","text":"从字符串JSON取出"}')).toBe("从字符串JSON取出");
});
it("keeps the previous frame height so a remount does not collapse the page", () => {
expect(presentFrameSlotHtml("pf9", 1280)).toContain('style="min-height:1280px"');
expect(presentFrameSlotHtml("pf9")).not.toContain("min-height");
expect(presentFrameSlotHtml("pf9", 4)).not.toContain("min-height");
});
it("normalizes sample data into present.v1 blocks", () => {
expect(previewPacket({ schema: "present.v1", blocks: { body: "x" } })).toEqual({
schema: "present.v1",
@@ -61,4 +169,43 @@ describe("present frame srcdoc", () => {
});
expect(previewPacket(null)).toEqual({ schema: "present.v1", blocks: {} });
});
it("accepts Chinese block ids and expands nested matrix JSON into a table", () => {
const srcdoc = buildPresentSrcdoc({
html: `<section data-block="卷宗表"></section><article data-block="正文"></article>`,
css: "",
js: "present.onData(function () {});",
packet: {
schema: "present.v1",
blocks: {
卷宗表: {
source: "heroines_matrix",
rows: [
{ name: "塞琳娜", 亲密度: -10, 服从度: 10 },
{ name: "崔岚", 亲密度: -10, 服从度: 10 },
],
},
正文: "第一段\n\n第二段",
},
},
slots: [
{ id: "卷宗表", type: "table" },
{ id: "正文", type: "markdown" },
],
frameId: "pf-zh",
});
expect(srcdoc).toContain("function isBlockId");
expect(srcdoc).toContain("present-data-table");
expect(srcdoc).toContain("matrixMapToRows");
expect(srcdoc).not.toContain("/^[A-Za-z_]");
const writeMatch = srcdoc.match(/function writeDataTable\(el, rows, columns\) \{[\s\S]*?\n \}/);
expect(writeMatch).toBeTruthy();
const idMatch = srcdoc.match(/function isBlockId\(id\) \{[\s\S]*?\n \}/);
expect(idMatch).toBeTruthy();
const isBlockId = new Function(`${idMatch![0]}; return isBlockId;`)();
expect(isBlockId("正文")).toBe(true);
expect(isBlockId("body")).toBe(true);
expect(isBlockId('bad"id')).toBe(false);
});
});