docs(quick-260329-ton): Markdown in Companion-Texten zu HTML rendern

This commit is contained in:
2026-03-29 21:27:18 +02:00
parent aa84df7d87
commit ed7bc11664
@@ -0,0 +1,139 @@
---
phase: quick
plan: 260329-ton
type: execute
wave: 1
depends_on: []
files_modified:
- src/utils/markdown.ts
- src/utils/markdown.test.ts
- src/app.ts
- src/ui/screens.ts
- src/forest/reward.ts
autonomous: true
requirements: []
must_haves:
truths:
- "AI-generated companion texts with *italic* render as <em> in the UI"
- "AI-generated companion texts with **bold** render as <strong> in the UI"
- "Plain text without markdown renders unchanged"
- "HTML special characters in generated text are escaped (no XSS)"
artifacts:
- path: "src/utils/markdown.ts"
provides: "renderInlineMarkdown() function"
exports: ["renderInlineMarkdown"]
- path: "src/utils/markdown.test.ts"
provides: "Unit tests for markdown rendering"
key_links:
- from: "src/app.ts"
to: "src/utils/markdown.ts"
via: "innerHTML = renderInlineMarkdown(text)"
pattern: "renderInlineMarkdown"
- from: "src/ui/screens.ts"
to: "src/utils/markdown.ts"
via: "innerHTML = renderInlineMarkdown(introText)"
pattern: "renderInlineMarkdown"
- from: "src/forest/reward.ts"
to: "src/utils/markdown.ts"
via: "innerHTML = renderInlineMarkdown(comment)"
pattern: "renderInlineMarkdown"
---
<objective>
Render inline Markdown (*italic*, **bold**) in AI-generated companion texts as HTML instead of displaying raw asterisks.
Purpose: AI-generated texts (greetings, letter intros, forest comments) contain Markdown formatting like *f* for emphasis, but `.textContent` displays them as plaintext with visible asterisks. Switching to `.innerHTML` with a safe inline markdown renderer fixes this.
Output: A `renderInlineMarkdown()` utility function used at all 3 AI-text insertion points.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/STATE.md
@src/utils/markdown.ts (will be created)
@src/app.ts
@src/ui/screens.ts
@src/forest/reward.ts
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Create renderInlineMarkdown utility with tests</name>
<files>src/utils/markdown.ts, src/utils/markdown.test.ts</files>
<behavior>
- renderInlineMarkdown("hello") returns "hello" (plain text unchanged)
- renderInlineMarkdown("press *f*") returns "press &lt;em&gt;f&lt;/em&gt;" (single asterisks become em)
- renderInlineMarkdown("very **important**") returns "very &lt;strong&gt;important&lt;/strong&gt;" (double asterisks become strong)
- renderInlineMarkdown("**bold** and *italic*") handles both in one string
- renderInlineMarkdown("a &lt;script&gt;alert(1)&lt;/script&gt;") escapes HTML entities BEFORE markdown processing (XSS safe)
- renderInlineMarkdown("2 * 3 * 4") does NOT wrap standalone asterisks with spaces (only wrapping content like *word*)
- renderInlineMarkdown("") returns "" (empty string)
</behavior>
<action>
Create src/utils/markdown.ts exporting a single function renderInlineMarkdown(text: string): string.
Implementation approach:
1. First escape HTML special chars: & < > " ' (using string replace chains, no DOM needed)
2. Then apply regex for **bold**: /\*\*(.+?)\*\*/g -> <strong>$1</strong>
3. Then apply regex for *italic*: /\*(.+?)\*/g -> <em>$1</em>
(Order matters: bold first so ** is not consumed as two single *)
No external dependencies. Pure function, no DOM.
Create src/utils/markdown.test.ts with vitest tests covering all behaviors above.
</action>
<verify>
<automated>cd /home/dev/workspace/zauberwald && npx vitest run src/utils/markdown.test.ts</automated>
</verify>
<done>All markdown rendering tests pass. Function handles bold, italic, mixed, HTML escaping, and edge cases.</done>
</task>
<task type="auto">
<name>Task 2: Replace .textContent with .innerHTML + renderInlineMarkdown at all AI-text insertion points</name>
<files>src/app.ts, src/ui/screens.ts, src/forest/reward.ts</files>
<action>
Import renderInlineMarkdown from "../utils/markdown" (or adjust relative path) in each file and replace .textContent with .innerHTML + renderInlineMarkdown() at these 3 specific locations:
1. src/app.ts line 227:
BEFORE: textEl.textContent = text;
AFTER: textEl.innerHTML = renderInlineMarkdown(text);
2. src/ui/screens.ts line 422:
BEFORE: companionText.textContent = introText;
AFTER: companionText.innerHTML = renderInlineMarkdown(introText);
3. src/forest/reward.ts line 207:
BEFORE: companionTextEl.textContent = comment;
AFTER: companionTextEl.innerHTML = renderInlineMarkdown(comment);
IMPORTANT: Do NOT change any other .textContent assignments in these files. The other assignments use hardcoded German strings (no markdown) or set empty strings. Only the 3 lines above receive AI-generated text that may contain markdown.
</action>
<verify>
<automated>cd /home/dev/workspace/zauberwald && npx vitest run && npx tsc --noEmit</automated>
</verify>
<done>All 3 AI-text insertion points use innerHTML + renderInlineMarkdown. TypeScript compiles without errors. Existing tests still pass.</done>
</task>
</tasks>
<verification>
- npx vitest run passes (all tests including new markdown tests)
- npx tsc --noEmit passes (no type errors)
- grep -n "renderInlineMarkdown" src/app.ts src/ui/screens.ts src/forest/reward.ts shows exactly 3 usage sites (plus imports)
</verification>
<success_criteria>
- AI-generated companion texts with *italic* or **bold** markdown render as styled HTML
- Plain text and hardcoded strings are unaffected
- HTML in generated text is escaped (no injection risk)
- All existing tests continue to pass
</success_criteria>
<output>
After completion, create `.planning/quick/260329-ton-markdown-in-companion-texten-zu-html-ren/260329-ton-SUMMARY.md`
</output>