Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/prd.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,16 @@ How the goals will be measured.
`;
}

// The seed marker is a hidden HTML comment, so a `-->` inside the idea would
// close it early and render the rest of the idea (plus the real closer) as
// visible text at the top of the Problem section. Neutralise the terminator.
function commentSafe(text) {
return String(text).replace(/--+>/g, (run) => `${run.slice(0, -1)}>`);
}

/** A numbered PRD body (prd/NNNN-slug.md), seeded from an idea. */
export function renderPrd({ id, title, idea, author }) {
const seed = idea && idea !== title ? `\n<!-- seed: ${idea} -->` : "";
const seed = idea && idea !== title ? `\n<!-- seed: ${commentSafe(idea)} -->` : "";
return `---
openprd: "${OPENPRD.version}"
id: "${id}"
Expand Down
31 changes: 31 additions & 0 deletions test/prd.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,34 @@ test("regenerateIndex escapes a pipe in a title so the row keeps its columns", (
fs.rmSync(root, { recursive: true, force: true });
}
});

test("renderPrd keeps a `-->` in the idea from closing the seed comment", () => {
const idea = "add a dark mode toggle <!-- oops --> and then some more idea text";
const body = renderPrd({ id: "0007", title: "Add a dark mode toggle", idea, author: "you@example.com" });

const lines = body.split(/\r?\n/);
const at = lines.findIndex((l) => l.startsWith("<!-- seed:"));
assert.notEqual(at, -1, "the seed marker must be on its own line");
const seedLine = lines[at];
// Exactly one comment terminator: the one this template owns, at the end.
assert.equal(seedLine.match(/-->/g).length, 1, `seed comment closes early: ${seedLine}`);
assert.ok(seedLine.endsWith(" and then some more idea text -->"), seedLine);
assert.ok(seedLine.includes("<!-- oops --&gt;"), `idea terminator not neutralised: ${seedLine}`);
// The Problem section past the marker is untouched template text.
assert.match(lines[at + 1], /^_Describe the user\/business problem/);
});

test("createPrd hides the whole idea even when it contains a comment terminator", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "moshcode-prd-"));
try {
const { path: file } = createPrd("ship a parser for --> arrows in specs", root);

const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
const at = lines.findIndex((l) => l.startsWith("<!-- seed:"));
assert.notEqual(at, -1, "createPrd must write a seed marker for a truncated idea");
assert.equal(lines[at].match(/-->/g).length, 1, `seed comment closes early: ${lines[at]}`);
assert.match(lines[at + 1], /^_Describe the user\/business problem/);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
Loading