|
| 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