Auftragsbuch: Konzept-Ansicht fuer IDE-Projekte (KONZEPT.md inline)
IDE-Vorbereiten-Karten bekommen einen 'Konzept'-Knopf: MC2 holt KONZEPT.md aus dem vorbereiteten Gitea-Repo (Gitea-Raw-API + Auth aus git-credentials) und rendert es direkt im Auftragsbuch (leichter MD-Renderer lib/markdown.tsx). So liest der Commander das ausgearbeitete Konzept in der GUI, ohne das Repo aufzumachen (User-Wunsch: 'lesen gehoert in MC2, feilen in die IDE'). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// Leichtgewichtiger Markdown-Renderer (Zeilen-basiert, XSS-frei da reines JSX,
|
||||
// bewusst ohne externe Lib). Überschriften, Listen, fett/kursiv, `code`, Codeblöcke.
|
||||
// Verwandt mit dem wiki-link-Renderer in WissenView, hier aber ohne [[links]] —
|
||||
// gedacht für Doku-Anzeige (z. B. das KONZEPT.md eines vorbereiteten IDE-Projekts).
|
||||
export function Markdown({ text, className }: { text: string; className?: string }) {
|
||||
const lines = (text || "").split("\n")
|
||||
const out: React.ReactNode[] = []
|
||||
let list: React.ReactNode[] = []
|
||||
let code: string[] | null = null
|
||||
|
||||
const flushList = (key: string) => {
|
||||
if (list.length) {
|
||||
out.push(<ul key={key} className="mb-3 ml-4 list-disc space-y-1">{list}</ul>)
|
||||
list = []
|
||||
}
|
||||
}
|
||||
|
||||
lines.forEach((raw, i) => {
|
||||
const key = `l${i}`
|
||||
if (code !== null) {
|
||||
if (raw.trimEnd() === "```") {
|
||||
out.push(<pre key={key} className="mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px] leading-relaxed text-foreground/85">{code.join("\n")}</pre>)
|
||||
code = null
|
||||
} else code.push(raw)
|
||||
return
|
||||
}
|
||||
if (raw.trimStart().startsWith("```")) { flushList(key); code = []; return }
|
||||
|
||||
const line = raw.trimEnd()
|
||||
if (!line.trim()) { flushList(key); return }
|
||||
|
||||
const h = line.match(/^(#{1,4})\s+(.*)$/)
|
||||
if (h) {
|
||||
flushList(key)
|
||||
const level = h[1].length
|
||||
const cls = level === 1 ? "text-base font-bold mt-1 mb-2" : level === 2 ? "text-sm font-bold mt-4 mb-1.5" : "text-xs font-bold mt-3 mb-1 uppercase tracking-wide text-muted-foreground"
|
||||
out.push(<p key={key} className={cn(cls, "font-space text-foreground")}>{inline(h[2], key)}</p>)
|
||||
return
|
||||
}
|
||||
const li = line.match(/^\s*[-*•]\s+(.*)$/)
|
||||
if (li) {
|
||||
list.push(<li key={key} className="text-xs leading-relaxed text-foreground/85">{inline(li[1], key)}</li>)
|
||||
return
|
||||
}
|
||||
const ol = line.match(/^\s*\d+\.\s+(.*)$/)
|
||||
if (ol) {
|
||||
list.push(<li key={key} className="text-xs leading-relaxed text-foreground/85">{inline(ol[1], key)}</li>)
|
||||
return
|
||||
}
|
||||
flushList(key)
|
||||
out.push(<p key={key} className="mb-2 text-xs leading-relaxed text-foreground/85">{inline(line, key)}</p>)
|
||||
})
|
||||
flushList("end")
|
||||
if (code !== null) out.push(<pre key="code-end" className="mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px]">{(code as string[]).join("\n")}</pre>)
|
||||
|
||||
return <div className={cn("max-w-3xl", className)}>{out}</div>
|
||||
}
|
||||
|
||||
// Inline: **fett**, *kursiv*, `code` — per Regex-Split, reines JSX (kein HTML-Inject).
|
||||
function inline(text: string, keyBase: string): React.ReactNode[] {
|
||||
const parts = text.split(/(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g)
|
||||
return parts.map((p, i) => {
|
||||
const key = `${keyBase}-${i}`
|
||||
if (/^\*\*[^*]+\*\*$/.test(p)) return <b key={key} className="font-semibold text-foreground">{p.slice(2, -2)}</b>
|
||||
if (/^\*[^*]+\*$/.test(p)) return <i key={key}>{p.slice(1, -1)}</i>
|
||||
if (/^`[^`]+`$/.test(p)) return <code key={key} className="rounded bg-background/50 px-1 font-mono text-[11px] text-foreground/90">{p.slice(1, -1)}</code>
|
||||
return <span key={key}>{p}</span>
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user