From 6bb53889da9f597eeb5dfa7028d57c96e7cbcc69 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 66f346d..105165d 100644 --- a/internal/gitea/client.go +++ b/internal/gitea/client.go @@ -751,6 +751,45 @@ func (c *Client) PostComment(ctx context.Context, token, owner, repo string, ind return &comment, 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 b67ad6b..588c1f9 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -366,6 +366,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 { @@ -376,12 +387,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, } @@ -417,6 +430,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 { @@ -426,11 +450,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}}