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;