From 2c32e1c6aa794da6c045fdcb9d378a365d91a6ac Mon Sep 17 00:00:00 2001 From: agent-company Date: Thu, 26 Mar 2026 17:10:12 +0000 Subject: [PATCH] feat: render issue and PR body as markdown via Gitea API Add RenderMarkdown method to gitea client that calls POST /api/v1/markdown to convert raw markdown text to safe HTML. Wire it into IssueDetail and PullDetail handlers to render body content as formatted markdown. Falls back gracefully to plain text if the API call fails. Templates updated to use RenderedBody (template.HTML) with fallback to raw Issue.Body/Pull.Body when rendering fails. Closes leeworks-agents/gitea-mobile#35 Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/gitea/client.go | 39 ++++++++++++++++++++++++++++ internal/handlers/handlers.go | 30 +++++++++++++++++++-- internal/templates/issue_detail.html | 4 ++- internal/templates/pull_detail.html | 4 ++- 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/internal/gitea/client.go b/internal/gitea/client.go index 9e98b7f..565018c 100644 --- a/internal/gitea/client.go +++ b/internal/gitea/client.go @@ -706,6 +706,45 @@ func (c *Client) SubmitReview(ctx context.Context, token, owner, repo string, in return nil } +// RenderMarkdown renders raw markdown text to HTML using the Gitea API. +// Falls back to the raw text if the API call fails. +func (c *Client) RenderMarkdown(ctx context.Context, token, text string) (string, error) { + payload, err := json.Marshal(map[string]string{ + "Text": text, + "Mode": "gfm", + }) + if err != nil { + return text, fmt.Errorf("marshaling markdown request: %w", err) + } + + url := c.baseURL + "/api/v1/markdown" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(payload))) + if err != nil { + return text, fmt.Errorf("creating markdown request: %w", err) + } + + req.Header.Set("Authorization", "token "+token) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "text/html") + + resp, err := c.httpClient.Do(req) + if err != nil { + return text, fmt.Errorf("executing markdown request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return text, fmt.Errorf("markdown API error %d", resp.StatusCode) + } + + rendered, err := io.ReadAll(resp.Body) + if err != nil { + return text, fmt.Errorf("reading markdown response: %w", err) + } + + return string(rendered), nil +} + // priorityScore returns a numeric score for sorting (lower = higher priority). func priorityScore(labels []string) int { for _, l := range labels { diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 30f68af..1e372b9 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -367,6 +367,17 @@ func (h *Handler) IssueDetail(w http.ResponseWriter, r *http.Request) { labels = nil } + // Render markdown body if present. + var renderedBody template.HTML + if issue.Body != "" { + rendered, err := h.Client.RenderMarkdown(r.Context(), token, issue.Body) + if err != nil { + slog.Warn("failed to render issue body markdown, using plain text", "error", err) + } else { + renderedBody = template.HTML(rendered) + } + } + // Build the content HTML using the template. tmpl, err := template.ParseFiles("internal/templates/issue_detail.html") if err != nil { @@ -377,12 +388,14 @@ func (h *Handler) IssueDetail(w http.ResponseWriter, r *http.Request) { type templateData struct { Issue *giteaclient.Issue + RenderedBody template.HTML Comments []giteaclient.Comment AvailableLabels []giteaclient.Label } data := templateData{ Issue: issue, + RenderedBody: renderedBody, Comments: comments, AvailableLabels: labels, } @@ -418,6 +431,17 @@ func (h *Handler) PullDetail(w http.ResponseWriter, r *http.Request) { return } + // Render markdown body if present. + var renderedBody template.HTML + if pr.Body != "" { + rendered, err := h.Client.RenderMarkdown(r.Context(), token, pr.Body) + if err != nil { + slog.Warn("failed to render PR body markdown, using plain text", "error", err) + } else { + renderedBody = template.HTML(rendered) + } + } + // Build the content HTML using the template. tmpl, err := template.ParseFiles("internal/templates/pull_detail.html") if err != nil { @@ -427,11 +451,13 @@ func (h *Handler) PullDetail(w http.ResponseWriter, r *http.Request) { } type templateData struct { - Pull *giteaclient.PullRequest + Pull *giteaclient.PullRequest + RenderedBody template.HTML } data := templateData{ - Pull: pr, + Pull: pr, + RenderedBody: renderedBody, } var buf strings.Builder diff --git a/internal/templates/issue_detail.html b/internal/templates/issue_detail.html index 5ec2b62..8ad3038 100644 --- a/internal/templates/issue_detail.html +++ b/internal/templates/issue_detail.html @@ -9,7 +9,9 @@ {{.Name}} {{end}} - {{if .Issue.Body}} + {{if .RenderedBody}} +
{{.RenderedBody}}
+ {{else if .Issue.Body}}
{{.Issue.Body}}
{{end}} diff --git a/internal/templates/pull_detail.html b/internal/templates/pull_detail.html index 3f4a5b9..1fea88a 100644 --- a/internal/templates/pull_detail.html +++ b/internal/templates/pull_detail.html @@ -15,7 +15,9 @@ -{{.Pull.Deletions}} {{if .Pull.Mergeable}}Mergeable{{end}} - {{if .Pull.Body}} + {{if .RenderedBody}} +
{{.RenderedBody}}
+ {{else if .Pull.Body}}
{{.Pull.Body}}
{{end}} -- 2.52.0