diff --git a/appview/pages/funcmap.go b/appview/pages/funcmap.go index 2a0c8ee6..4997d0bb 100644 --- a/appview/pages/funcmap.go +++ b/appview/pages/funcmap.go @@ -40,6 +40,12 @@ import ( type tab map[string]string +type MarkdownSegment struct { + HTML template.HTML + IsCode bool + Lines int +} + func (p *Pages) funcMap() template.FuncMap { return template.FuncMap{ "split": func(s string) []string { @@ -306,6 +312,13 @@ func (p *Pages) funcMap() template.FuncMap { sanitized := rctx.SanitizeDefault(htmlString) return template.HTML(sanitized) }, + "markdownSegments": func(text string) []MarkdownSegment { + rctx := p.rctx.Clone() + rctx.RendererType = markup.RendererTypeDefault + htmlString := rctx.RenderMarkdown(text) + sanitized := rctx.SanitizeDefault(htmlString) + return splitCodeBlockSegments(string(sanitized)) + }, "description": func(text string) template.HTML { rctx := p.rctx.Clone() rctx.RendererType = markup.RendererTypeDefault @@ -625,6 +638,73 @@ func (p *Pages) AvatarUrl(actor, size string) string { return baseUrl } +func splitCodeBlockSegments(html string) []MarkdownSegment { + var segments []MarkdownSegment + remaining := html + closeTag := `` + + for { + idx := strings.Index(remaining, ``) + if openEnd == -1 { + remaining = remaining[idx:] + continue + } + openEnd += idx + openTag := remaining[idx : openEnd+1] + + if strings.Contains(openTag, `mermaid`) { + closeIdx := strings.Index(remaining[openEnd+1:], closeTag) + if closeIdx == -1 { + remaining = remaining[idx:] + continue + } + closeIdx += openEnd + 1 + + if idx > 0 { + segments = append(segments, MarkdownSegment{HTML: template.HTML(remaining[:idx])}) + } + + preEnd := closeIdx + len(closeTag) + preHTML := remaining[idx:preEnd] + segments = append(segments, MarkdownSegment{ + HTML: template.HTML(preHTML), + }) + remaining = remaining[preEnd:] + continue + } + + closeIdx := strings.Index(remaining[openEnd+1:], closeTag) + if closeIdx == -1 { + remaining = remaining[idx:] + continue + } + closeIdx += openEnd + 1 + + if idx > 0 { + segments = append(segments, MarkdownSegment{HTML: template.HTML(remaining[:idx])}) + } + + preEnd := closeIdx + len(closeTag) + preHTML := remaining[idx:preEnd] + content := remaining[openEnd+1 : closeIdx] + lines := strings.Count(content, "\n") + 1 + segments = append(segments, MarkdownSegment{ + HTML: template.HTML(preHTML), + IsCode: true, + Lines: lines, + }) + remaining = remaining[preEnd:] + } + return segments +} + func (p *Pages) icon(name string, classes []string) (template.HTML, error) { iconPath := filepath.Join("static", "icons", name) diff --git a/appview/pages/templates/fragments/comment/commentBody.html b/appview/pages/templates/fragments/comment/commentBody.html index 2a8d35e8..35cb5ae1 100644 --- a/appview/pages/templates/fragments/comment/commentBody.html +++ b/appview/pages/templates/fragments/comment/commentBody.html @@ -1,7 +1,7 @@ {{ define "fragments/comment/commentBody" }}
{{ if not .Comment.Deleted }} -
{{ .Comment.Body.Text | markdown }}
+
{{ template "fragments/markdown/body" (dict "Segments" (.Comment.Body.Text | markdownSegments)) }}
{{ template "repo/fragments/reactions" (dict "Reactions" .Reactions "UserReacted" .UserReacted diff --git a/appview/pages/templates/fragments/markdown/body.html b/appview/pages/templates/fragments/markdown/body.html new file mode 100644 index 00000000..d03e9c44 --- /dev/null +++ b/appview/pages/templates/fragments/markdown/body.html @@ -0,0 +1,70 @@ +{{ define "fragments/markdown/body" }} +{{ range $seg := .Segments }} +{{ if $seg.IsCode }} +{{ if ge $seg.Lines 28 }} +
+
{{ $seg.HTML }}
+
+
+ + {{ $seg.Lines }} lines +
+
+ + +
+
+
+{{ else }} +{{ $seg.HTML }} +{{ end }} +{{ else }} +{{ $seg.HTML }} +{{ end }} +{{ end }} + +{{ end }} diff --git a/appview/pages/templates/fragments/markdownPreview.html b/appview/pages/templates/fragments/markdownPreview.html index b65f13b3..9a4d5043 100644 --- a/appview/pages/templates/fragments/markdownPreview.html +++ b/appview/pages/templates/fragments/markdownPreview.html @@ -1,7 +1,7 @@ {{ define "fragments/markdownPreview" }} {{ if . }}
- {{ . | markdown }} + {{ template "fragments/markdown/body" (dict "Segments" (. | markdownSegments)) }}
{{ else }}
Nothing to preview.
diff --git a/appview/pages/templates/repo/issues/issue.html b/appview/pages/templates/repo/issues/issue.html index 12039bf6..d1e437ff 100644 --- a/appview/pages/templates/repo/issues/issue.html +++ b/appview/pages/templates/repo/issues/issue.html @@ -33,7 +33,7 @@ {{ template "issueHeader" .Issue }} {{ template "issueInfo" . }} {{ if .Issue.Body }} -
{{ .Issue.Body | markdown }}
+
{{ template "fragments/markdown/body" (dict "Segments" (.Issue.Body | markdownSegments)) }}
{{ end }}
{{ $aturi := .Issue.AtUri }} diff --git a/appview/pages/templates/repo/pulls/fragments/pullHeader.html b/appview/pages/templates/repo/pulls/fragments/pullHeader.html index 182a8451..27cfcf30 100644 --- a/appview/pages/templates/repo/pulls/fragments/pullHeader.html +++ b/appview/pages/templates/repo/pulls/fragments/pullHeader.html @@ -44,7 +44,7 @@ {{ if .Pull.Body }}
- {{ .Pull.Body | markdown }} + {{ template "fragments/markdown/body" (dict "Segments" (.Pull.Body | markdownSegments)) }}
{{ end }} diff --git a/input.css b/input.css index 412f6727..557e4474 100644 --- a/input.css +++ b/input.css @@ -201,7 +201,7 @@ @layer components { .btn { @apply relative z-10 inline-flex overflow-hidden items-center justify-center - min-h-[32px] px-2 py-[6px] + min-h-[32px] px-2 py-[6px] gap-1.5 rounded border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm text-gray-900 dark:text-gray-100 @@ -367,6 +367,29 @@ @apply flex justify-center my-4 overflow-x-auto bg-transparent border-0; } + /* Code block collapse */ + .code-block-wrapper { + @apply flex flex-col max-h-[600px] overflow-hidden border border-gray-200 rounded-md dark:border-gray-700; + + &:has(.code-block-expand:checked) { + @apply max-h-none overflow-visible; + + .code-block-pre-container { + overflow-y: visible; + } + } + + :where(pre) { + @apply m-0 border-none rounded-none dark:bg-transparent; + } + } + .code-block-pre-container { + @apply overflow-y-hidden flex-1 bg-gray-50 dark:bg-gray-900; + } + .code-block-actions { + @apply flex items-center justify-between border-t border-gray-200 bg-white p-2 shrink-0 dark:border-gray-700 dark:bg-gray-800; + } + /* Base callout */ details[data-callout] { @apply border-l-4 pl-3 py-2 text-gray-800 dark:text-gray-200 my-4;