feat: add kitchen view command and parser skeleton (task 0003)
Cut the first vertical slice through both layers: a runnable `kitchen view <file>` that reads a Recipe File, parses it, and renders it styled to the terminal. @kitchen-md/core exposes a pure, total `parse(input): DocumentAST` built on a minimal remark pipeline (parse + frontmatter) with a translation layer to core's own AST types — frontmatter passthrough, HeadingBlock, ParagraphBlock, and TextNode. remark types never leak into the public API. @kitchen-md/bin's `view` subcommand (commander) reads the file and hands the DocumentAST to a pure `render(ast): string` (chalk, ANSI auto-suppressed off a TTY). Fallible file I/O is modelled as a neverthrow Result over a tagged-union CliError, matched at the boundary: stdout on success, stderr and exit 1 on failure. The command functions sit beside the import.meta.main-guarded CLI entry so they are testable in-process. Tests follow ADR 0009's tiers — unit (parse, render), integration (the view command's Result), and e2e (the binary as a subprocess) — with coverage, a path-scoped test-report generator, and a prose fixture rounding out the tooling. ADR 0008 records errors-as-values at the CLI boundary; ADR 0009 records the testing tiers.
This commit was merged in pull request #2.
This commit is contained in:
102
packages/core/src/parse_test.ts
Normal file
102
packages/core/src/parse_test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { parse } from "@kitchen-md/core";
|
||||
|
||||
describe("parse", () => {
|
||||
test("frontmatter passthrough — arbitrary fields become a plain object", () => {
|
||||
const input = `---
|
||||
title: Buttered Toast
|
||||
servings: 2
|
||||
tags: [breakfast, simple]
|
||||
---
|
||||
|
||||
# Buttered Toast
|
||||
`;
|
||||
|
||||
const result = parse(input);
|
||||
|
||||
expect(result.frontmatter).toEqual({
|
||||
title: "Buttered Toast",
|
||||
servings: 2,
|
||||
tags: ["breakfast", "simple"],
|
||||
});
|
||||
});
|
||||
|
||||
test("frontmatter is an empty object when absent", () => {
|
||||
const result = parse("# Just a heading");
|
||||
|
||||
expect(result.frontmatter).toEqual({});
|
||||
});
|
||||
|
||||
test("frontmatter is an empty object when the block is empty", () => {
|
||||
const input = `---
|
||||
---
|
||||
|
||||
# Heading
|
||||
`;
|
||||
|
||||
const result = parse(input);
|
||||
|
||||
expect(result.frontmatter).toEqual({});
|
||||
});
|
||||
|
||||
test("headings are modelled at every level 1–6 with TextNode children", () => {
|
||||
const cases: [string, 1 | 2 | 3 | 4 | 5 | 6, string][] = [
|
||||
["# Level One", 1, "Level One"],
|
||||
["## Level Two", 2, "Level Two"],
|
||||
["### Level Three", 3, "Level Three"],
|
||||
["#### Level Four", 4, "Level Four"],
|
||||
["##### Level Five", 5, "Level Five"],
|
||||
["###### Level Six", 6, "Level Six"],
|
||||
];
|
||||
|
||||
for (const [markdown, level, text] of cases) {
|
||||
const result = parse(markdown);
|
||||
|
||||
expect(result.blocks).toEqual([
|
||||
{
|
||||
type: "heading",
|
||||
level,
|
||||
children: [{ type: "text", value: text }],
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test("a paragraph is a ParagraphBlock with TextNode content", () => {
|
||||
const result = parse("Just some plain prose.");
|
||||
|
||||
expect(result.blocks).toEqual([
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [{ type: "text", value: "Just some plain prose." }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("blocks are flat and in document order — a heading is a sibling of the following paragraph", () => {
|
||||
const input = `# Title
|
||||
|
||||
A paragraph under it.
|
||||
`;
|
||||
|
||||
const result = parse(input);
|
||||
|
||||
expect(result.blocks).toEqual([
|
||||
{
|
||||
type: "heading",
|
||||
level: 1,
|
||||
children: [{ type: "text", value: "Title" }],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [{ type: "text", value: "A paragraph under it." }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("diagnostics are empty in the normal case", () => {
|
||||
const result = parse("# Ok");
|
||||
|
||||
expect(result.diagnostics).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user