feat(kebab-tui): p9-fb-11 ask answer markdown rendering #72
Reference in New Issue
Block a user
Delete Branch "feat/p9-fb-11-markdown"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
요약
도그푸딩 item 9 — TUI Ask 답변 본문이 raw markdown 그대로 보여 가독성 떨어지던 문제 해소. pulldown-cmark 로 파싱 → ratatui Span/Line + p9-fb-14 Theme 변환. CLI 는 raw markdown 그대로 (terminal 호환성).
변경
kebab-tui::markdown::render(text, &Theme) -> Vec<Line<'static>>신규. pulldown-cmark 0.13 위에 build (이미 kebab-parse-md 가 사용 중).inline: bold/italic/strikethrough → modifier, inline code →
Role::Hint, link →Role::CitationMarker + UNDERLINED.block: heading H1/H2 →
Role::Heading, H3+ →Role::Title, list (bullet/ordered/nested), fenced code → indent +Role::Hint, blockquote▎, table|-separated,---→ 가로선.streaming 안전성: 매 frame 재 parse, unterminated
**등 incomplete inline 은 literal 글자 그대로 (글자 누락 X).ask::push_turn_linesgrounded 답변만 markdown 렌더. refusal/streaming 은 role color 시그널 보존 위해 raw. body 들은indent.CLI
kebab ask는 raw markdown 그대로 (변경 없음).테스트
cargo clippy -p kebab-tui --all-targets -- -D warningsclean문서
kebab tui행: markdown 렌더 + CLI raw 명시도그푸딩 item 9 — TUI Ask 답변 본문이 raw `**bold**` / `# Title` / ` ```code``` ` 그대로 보여 가독성 떨어지던 문제 해소. pulldown-cmark 파싱 → ratatui Span/Line 변환. ## 핵심 변경 - **`kebab-tui::markdown::render(text, &Theme) -> Vec<Line<'static>>`** 신규. pulldown-cmark = "0.13" (이미 kebab-parse-md 가 사용 중인 버전) 위에 build. inline: - `**bold**` / `__bold__` → `Modifier::BOLD` - `*italic*` / `_italic_` → `Modifier::ITALIC` - `~~strike~~` → `Modifier::CROSSED_OUT` - `` `code` `` → `Role::Hint` (DIM 스타일 — 터미널 호환성 위해 bg color 보다 안전) - `[text](url)` → `Role::CitationMarker` + `Modifier::UNDERLINED` block: - heading H1/H2 → `Role::Heading` (Cyan + BOLD), H3-H6 → `Role::Title` (White + BOLD) - bullet list `-`/`*` → `- ` + 깊이별 indent - ordered list `1.` → 실제 번호 prefix + indent - fenced code block ``` ``` ``` → ` ` indented + `Role::Hint` - blockquote `>` → 좌측 `▎` bar (중첩 시 반복) + `Role::Hint` - table `| col |` → `| col1 | col2 |` 식 줄, `|` separator 색 강조 - horizontal rule `---` → `─` × 40 - **streaming 안전성**: 매 frame 재 parse 가 spec — pulldown 토크나이저가 µs/KB 라 비용 무시. unterminated `**` (사용자가 한창 입력 중인 inline 가 닫히기 전) 은 pulldown 이 Text 로 처리 → literal `**` 그대로 표시 (글자 누락 X). - **`ask::push_turn_lines` 통합**: grounded 답변에서만 markdown 렌더 사용. refusal turn (`Role::Warning` override) 와 streaming turn (`Role::Hint`) 은 raw 로 두어 role color 시그널이 markdown 스타일에 묻히지 않도록. body line 들은 ` ` indent 로 transcript 에서 답변 본문 시각 구분. - **CLI `kebab ask` 출력은 raw markdown** — 터미널 호환성 + pipe 처리 시 안정성 위해 (ANSI escape 없이 plain text). ## 테스트 (markdown.rs 14 unit) - empty input → 빈 라인 1 줄 (caller scroll/measure 안전) - plain text → 단일 라인 + paragraph blank - bold / italic / strikethrough / inline code → 해당 modifier 검증 - link → UNDERLINED 검증 - heading H1 → BOLD 텍스트 span - bullet list `-` / numbered list `1./2.` → prefix 검증 - code fence body → 줄별 ` ` indent 보존 - blockquote → `▎` prefix - 2x2 table → `|`-separated 줄 검증 - unterminated `**` → 글자 누락 없음 (streaming 안전성 회귀 방지) - composite (heading + para + list + code) → 문서 순서 보존 기존 75 TUI 테스트 + 신규 14 markdown = 89 통과. clippy clean. ## 문서 - README `kebab tui` 행에 markdown 렌더 안내 + CLI 는 raw 명시 - HANDOFF: 2026-05-03 entry - spec status planned → in_progress Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>회차 1 — pulldown-cmark Event 매핑이 정석적이고 streaming 시 매 frame 재 parse 라는 결정도 합리적 (토크나이저 µs/KB). Role 매핑 (heading H1/H2 → Heading, H3+ → Title, link → CitationMarker+UNDERLINE, code → Hint) 도 p9-fb-14 의 role 의도와 잘 맞습니다.
actionable nit 3 건 inline. (a) heading 안의 link/code 가 heading 색에 가려짐, (b) Math/Footnote/TaskList silently drop 으로 답변 글자 사라짐 위험, (c) streaming 회귀 테스트의 OR 어설션이 약함.
@@ -0,0 +195,4 @@Event::Rule => {flush_current(&mut current, &mut out);out.push(Line::from(Span::styled("─".repeat(40),Event::FootnoteReference / TaskListMarker / InlineMath / DisplayMath4 종을 silently drop 하는데, 특히InlineMath와DisplayMath는 사용자가 답변에서 수학 표현을 봤을 때 아예 사라지는 현상이 됩니다 ($E = mc^2$가 빈 자리만 남음). 우선 raw 글자라도 살리는 게 정보 손실 측면에서 안전합니다:TaskListMarker(bool)는[ ]/[x]로 그대로 표시하면 되고,FootnoteReference도[^1]형태 raw 가 답변 본문에서 자연스럽습니다.@@ -0,0 +306,4 @@}let line: Vec<Span<'static>> = std::mem::take(current);out.push(Line::from(line));}compose_style의 분기 우선순위가 heading > link > inline_code > Body 인데, heading 안의 link / inline code 는 heading 스타일에 가려집니다. 예:# Section [docs](url)에서docs가 헤딩 스타일만 받고 underline 이 사라짐.2 가지 옵션:
저는 (1) 이 현재 코드 의도와 더 정렬한다고 봅니다 — link 의 UNDERLINE 은 색과 무관한 "클릭 가능한 텍스트" 시그널이라 헤딩에서도 살리는 게 자연스럽습니다.
@@ -0,0 +448,4 @@assert!(texts.iter().any(|t| t.starts_with("2. beta")));}/// Code fence body is preserved verbatim per line, indented two테스트
unterminated_bold_renders_literal_asterisks의 assertion 이 OR 로 묶여 있어 "실제 동작" 을 pin 하지 못합니다 (둘 중 하나만 통과해도 OK):현 pulldown-cmark 0.13 이 어느 쪽으로 emit 하는지 한 번 확인하고, 그 동작을 pin 하면 streaming 안전성 회귀 (예: pulldown 업그레이드가 글자 누락 새 버그 끌고 옴) 가 잡힙니다. 단순
combined.contains("still typing")(글자만 없으면 fail) 로 강화하면 충���합니다 —**보존 여부는 cosmetic, 글자 누락은 진짜 회귀.회차 2 — nit 3 건 깔끔히 반영.
추가 지적 없음. 머지 OK.