docs(quick-260329-ton): Markdown in Companion-Texten zu HTML rendern
This commit is contained in:
+139
@@ -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 <em>f</em>" (single asterisks become em)
|
||||||
|
- renderInlineMarkdown("very **important**") returns "very <strong>important</strong>" (double asterisks become strong)
|
||||||
|
- renderInlineMarkdown("**bold** and *italic*") handles both in one string
|
||||||
|
- renderInlineMarkdown("a <script>alert(1)</script>") 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>
|
||||||
Reference in New Issue
Block a user