Skip to content

Commit b2e17e5

Browse files
authored
feat(quoteforge[warnings]): warn when a template drops a block type it cannot render (#11)
1 parent 4a76c18 commit b2e17e5

3 files changed

Lines changed: 92 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Added
6+
- **`generate` and `slides` now warn when a template drops a block.** 15 of the 28 templates
7+
render only a subset of block types and silently discard the rest — a `headline` on `memo`,
8+
a `text` block on `ticket`, anything but `bullet-list`/`callout` on `grid`, `versus`, and
9+
`prompt`. The card validated, rendered, exited 0, and was missing content the author had
10+
written. The warning names the template and the dropped types. It reads each template's own
11+
`block.type` guards at runtime, so it cannot drift from the markup.
12+
313
## 1.1.0
414

515
### Changed

src/__tests__/template-warnings.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,64 @@ describe("templateWarnings", () => {
5555
test("stays silent when no eyebrow is set", () => {
5656
expect(templateWarnings({ template: "list", blocks: [] })).toEqual([]);
5757
});
58+
59+
test("warns when a template drops a block type it cannot render", () => {
60+
const [warning] = templateWarnings({
61+
template: "memo",
62+
blocks: [
63+
{ type: "headline" },
64+
{ type: "callout", items: [{}] },
65+
],
66+
});
67+
expect(warning).toContain("memo");
68+
expect(warning).toContain("headline");
69+
expect(warning).toContain("will not appear");
70+
});
71+
72+
test("names every dropped type once, without repeating duplicates", () => {
73+
const [warning] = templateWarnings({
74+
template: "prompt",
75+
blocks: [{ type: "headline" }, { type: "headline" }, { type: "text" }],
76+
});
77+
expect(warning).toContain("headline");
78+
expect(warning).toContain("text");
79+
expect((warning ?? "").match(/headline/g)).toHaveLength(1);
80+
});
81+
82+
test("stays silent when every block is one the template renders", () => {
83+
expect(
84+
templateWarnings({
85+
template: "memo",
86+
blocks: [{ type: "callout", items: [{}] }, { type: "text" }],
87+
}),
88+
).toEqual([]);
89+
});
90+
91+
test("stays silent for templates that render every block type", () => {
92+
expect(
93+
templateWarnings({
94+
template: "manifesto",
95+
blocks: [{ type: "headline" }, { type: "code" }, { type: "chart" }],
96+
}),
97+
).toEqual([]);
98+
});
99+
100+
test("says nothing about blocks when the template is unknown", () => {
101+
expect(
102+
templateWarnings({ template: "no-such-template", blocks: [{ type: "headline" }] }),
103+
).toEqual([]);
104+
});
105+
106+
test("every shipped example renders all of its own blocks", async () => {
107+
const { readdirSync, readFileSync } = await import("node:fs");
108+
const dir = "content/examples";
109+
for (const file of readdirSync(dir).filter((f) => f.endsWith(".json"))) {
110+
const card = JSON.parse(readFileSync(`${dir}/${file}`, "utf-8")) as {
111+
template: string;
112+
eyebrow?: string;
113+
blocks: { type: string }[];
114+
};
115+
expect([file, templateWarnings(card)]).toEqual([file, []]);
116+
}
117+
});
58118
});

src/renderer/template-warnings.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ function templateSource(template: string): string {
2424
}
2525
}
2626

27+
function handledBlockTypes(template: string): Set<string> | undefined {
28+
const matches = templateSource(template).matchAll(/block\.type\s*==\s*"([a-z-]+)"/g);
29+
const types = new Set([...matches].map((m) => m[1] as string));
30+
return types.size > 0 ? types : undefined;
31+
}
32+
2733
/**
2834
* Non-fatal layout advice. A card that trips one of these still renders — the
2935
* result just will not look like the template intends.
@@ -50,5 +56,21 @@ export function templateWarnings(card: WarnableCard): string[] {
5056
);
5157
}
5258

59+
const handled = handledBlockTypes(card.template);
60+
if (handled) {
61+
const dropped = [...new Set(card.blocks.map((b) => b.type))].filter(
62+
(type) => !handled.has(type),
63+
);
64+
65+
if (dropped.length > 0) {
66+
const list = dropped.map((type) => `"${type}"`).join(", ");
67+
warnings.push(
68+
`Template "${card.template}" does not render ${list} — that content will not appear ` +
69+
`in the image. Pick a template that handles it, or move the text into a block the ` +
70+
`template renders.`,
71+
);
72+
}
73+
}
74+
5375
return warnings;
5476
}

0 commit comments

Comments
 (0)