Skip to content

Commit 59b536f

Browse files
corvid-agentclaude
andauthored
docs: add table-editor.spec.md (#37)
Create spec for the table-editor component and markdown-table model, following the project constitution format. Documents component inputs/ outputs, all exported types and functions, invariants, behavioral examples, and error cases. Fixes #32 Co-authored-by: Claude Opus 4 <[email protected]>
1 parent 153a6da commit 59b536f

1 file changed

Lines changed: 151 additions & 0 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
---
2+
module: table-editor
3+
version: 1
4+
status: active
5+
files:
6+
- src/app/components/table-editor/table-editor.ts
7+
- src/app/components/table-editor/table-editor.html
8+
- src/app/components/table-editor/table-editor.scss
9+
- src/app/models/markdown-table.ts
10+
depends_on:
11+
- spec-models
12+
---
13+
14+
# Table Editor
15+
16+
## Purpose
17+
18+
Provides an inline structured editing interface for markdown tables within spec sections. Instead of editing raw pipe-delimited markdown, users interact with a grid of text inputs rendered in an HTML table. The component receives a parsed `MarkdownTable` and emits immutable updates on every cell change, row addition, deletion, or reorder. The companion `markdown-table` model handles parsing raw markdown into structured `MarkdownTable` data and serializing it back, as well as splitting section content into interleaved text and table `ContentBlock` segments.
19+
20+
## Public API
21+
22+
### Exported Classes
23+
24+
| Class | Description |
25+
|-------|-------------|
26+
| `TableEditorComponent` | Angular standalone component for inline editing of a single markdown table |
27+
28+
### Component Inputs
29+
30+
| Input | Type | Required | Description |
31+
|-------|------|----------|-------------|
32+
| `table` | `MarkdownTable` | Yes | The parsed table data to display and edit |
33+
34+
### Component Outputs
35+
36+
| Output | Type | Description |
37+
|--------|------|-------------|
38+
| `tableChange` | `MarkdownTable` | Emitted with a new immutable table object whenever any cell, row addition, deletion, or reorder occurs |
39+
40+
### Exported Types (markdown-table model)
41+
42+
| Type | Description |
43+
|------|-------------|
44+
| `MarkdownTable` | Interface with `headers: string[]` and `rows: string[][]` representing a parsed markdown table |
45+
| `ContentBlock` | Interface with `type: 'text' \| 'table'`, optional `text` for text blocks, optional `table` for table blocks |
46+
47+
### Exported Functions (markdown-table model)
48+
49+
| Function | Parameters | Returns | Description |
50+
|----------|-----------|---------|-------------|
51+
| `parseContentBlocks` | `(content: string)` | `ContentBlock[]` | Splits section markdown into interleaved text and table blocks |
52+
| `serializeContentBlocks` | `(blocks: ContentBlock[])` | `string` | Joins content blocks back into a single markdown string |
53+
| `parseMarkdownTable` | `(raw: string)` | `MarkdownTable \| null` | Parses a raw pipe-delimited markdown table string into structured data; returns null if invalid |
54+
| `serializeMarkdownTable` | `(table: MarkdownTable)` | `string` | Converts structured table data back to a pipe-delimited markdown string with aligned columns |
55+
56+
## Invariants
57+
58+
1. Every mutation (cell edit, add row, remove row, move row) emits a new `MarkdownTable` object — the original is never mutated
59+
2. Headers are read-only in the component — only row data can be edited
60+
3. New rows are always appended with empty strings matching the header count
61+
4. Row cells are padded or trimmed to match the header count during parsing
62+
5. `moveRowUp` is a no-op when the row index is 0; `moveRowDown` is a no-op when the row is last
63+
6. `parseMarkdownTable` requires at least a header row and a separator row (minimum 2 lines); returns null otherwise
64+
7. The separator row must match the pattern `|---...|---...|` (pipes and dashes, optional colons for alignment)
65+
8. `parseContentBlocks` identifies table lines as those starting with `|` and ending with `|` (after trimming whitespace)
66+
9. Table lines that fail to parse as a valid table are treated as plain text blocks
67+
10. `serializeMarkdownTable` pads columns to align pipes based on the widest cell in each column, with a minimum width of 3
68+
11. Row action buttons (move up, move down, remove) are only visible on row hover
69+
70+
## Behavioral Examples
71+
72+
### Scenario: Edit a cell value
73+
74+
- **Given** a table with headers `["Name", "Type"]` and one row `["id", "number"]`
75+
- **When** user changes the second cell to `"string"`
76+
- **Then** `tableChange` emits `{ headers: ["Name", "Type"], rows: [["id", "string"]] }`
77+
78+
### Scenario: Add a row
79+
80+
- **Given** a table with 2 headers and 1 existing row
81+
- **When** user clicks "+ Add Row"
82+
- **Then** `tableChange` emits with the original row plus a new row of `["", ""]`
83+
84+
### Scenario: Remove a row
85+
86+
- **Given** a table with rows at indices 0, 1, 2
87+
- **When** user clicks the remove button on row 1
88+
- **Then** `tableChange` emits with only rows 0 and 2
89+
90+
### Scenario: Move row up
91+
92+
- **Given** a table with rows `[["a"], ["b"], ["c"]]`
93+
- **When** user moves row at index 2 up
94+
- **Then** `tableChange` emits with rows `[["a"], ["c"], ["b"]]`
95+
96+
### Scenario: Move first row up (no-op)
97+
98+
- **Given** a table with rows `[["a"], ["b"]]`
99+
- **When** `moveRowUp(0)` is called
100+
- **Then** no event is emitted; the table remains unchanged
101+
102+
### Scenario: Empty table shows placeholder
103+
104+
- **Given** a table with headers but zero rows
105+
- **When** the component renders
106+
- **Then** an empty state message "No rows yet. Add one below." is displayed
107+
108+
### Scenario: Parse section content with mixed text and tables
109+
110+
- **Given** section content containing a paragraph, then a markdown table, then more text
111+
- **When** `parseContentBlocks(content)` is called
112+
- **Then** returns 3 blocks: text, table, text — in order
113+
114+
### Scenario: Round-trip serialization preserves content
115+
116+
- **Given** a markdown string with a valid table
117+
- **When** `parseContentBlocks` then `serializeContentBlocks` are called in sequence
118+
- **Then** the output is semantically equivalent to the input (column alignment may differ)
119+
120+
## Error Cases
121+
122+
| Condition | Behavior |
123+
|-----------|----------|
124+
| Table string with fewer than 2 lines | `parseMarkdownTable` returns `null` |
125+
| Missing separator row (no `---` pattern) | `parseMarkdownTable` returns `null` |
126+
| Row has fewer cells than headers | Cells are padded with empty strings |
127+
| Row has more cells than headers | Extra cells are trimmed to match header count |
128+
| Table lines that fail parsing | `parseContentBlocks` falls back to treating them as text blocks |
129+
| Table with zero headers | `serializeMarkdownTable` returns empty string |
130+
131+
## Dependencies
132+
133+
### Consumes
134+
135+
| Module | What is used |
136+
|--------|-------------|
137+
| `@angular/core` | `Component`, `input`, `output`, `computed` |
138+
| `@angular/forms` | `FormsModule` (for `ngModel` on cell inputs) |
139+
140+
### Consumed By
141+
142+
| Module | What is used |
143+
|--------|-------------|
144+
| `section-editor` | `TableEditorComponent` for inline table editing; `parseContentBlocks`, `serializeContentBlocks`, `MarkdownTable`, `ContentBlock` from `markdown-table` model |
145+
| `editor-page` | Indirectly via section-editor |
146+
147+
## Change Log
148+
149+
| Date | Author | Change |
150+
|------|--------|--------|
151+
| 2026-02-25 | CorvidAgent | Initial spec |

0 commit comments

Comments
 (0)