web frontend for git repositories, written in Go git.pocka.jp/legit.git
3

Configure Feed

Select the types of activity you want to include in your feed.

HTML preview for Markdown

While Markdown is relatively readable format, syntax highlighted
monospace font is not easy to read compared to well-spaced sans-serif.

Shota FUJI (Mar 29, 2025, 10:44 PM +0900) 5e20b033 d38d10b2

+360 -161
+17
routes/data.go
··· 121 121 122 122 // LineNumbers holds sequential numbers starting from 1 up to line count of the blob. 123 123 LineNumbers []uint 124 + 125 + // A list of preview output types. 126 + PreviewTypes []string 127 + } 128 + 129 + // repoBlobRefHTMLPreviewData is a data object passed to "repo-blob-ref-html-preview" template. 130 + type repoBlobRefHTMLPreviewData struct { 131 + // Config represents a resolved config based on "config.yaml". 132 + Config *config.Config 133 + 134 + Meta repositoryMeta 135 + 136 + // Path to the blob. 137 + Path []string 138 + 139 + // Rendered HTML 140 + Content template.HTML 124 141 } 125 142 126 143 // repoLogRefData is a data object passed to "repo-log-ref" template.
+42
routes/preview/renderer.go
··· 1 + // This file contains preview renderers. 2 + // 3 + // Copyright 2025 Shota FUJI <pockawoooh@gmail.com> 4 + // SPDX-License-Identifier: MIT 5 + 6 + package preview 7 + 8 + import ( 9 + "path/filepath" 10 + 11 + "github.com/microcosm-cc/bluemonday" 12 + "github.com/russross/blackfriday/v2" 13 + ) 14 + 15 + type Renderer interface { 16 + GetPreviewType() string 17 + 18 + Render(code []byte) ([]byte, error) 19 + } 20 + 21 + type MarkdownToHtmlRenderer struct{} 22 + 23 + func (r MarkdownToHtmlRenderer) GetPreviewType() string { 24 + return "html" 25 + } 26 + 27 + func (r MarkdownToHtmlRenderer) Render(code []byte) ([]byte, error) { 28 + sanitizer := bluemonday.UGCPolicy() 29 + unsafe := blackfriday.Run(code, blackfriday.WithExtensions(blackfriday.CommonExtensions)) 30 + return sanitizer.SanitizeBytes(unsafe), nil 31 + } 32 + 33 + func GetPreviewRenderers(fileName string) []Renderer { 34 + ext := filepath.Ext(fileName) 35 + 36 + switch ext { 37 + case ".md", ".mkd", ".markdown": 38 + return []Renderer{MarkdownToHtmlRenderer{}} 39 + default: 40 + return []Renderer{} 41 + } 42 + }
+66 -16
routes/routes.go
··· 17 17 "github.com/microcosm-cc/bluemonday" 18 18 "github.com/pocka/legit/config" 19 19 "github.com/pocka/legit/git" 20 + "github.com/pocka/legit/routes/preview" 20 21 "github.com/russross/blackfriday/v2" 21 22 ) 22 23 ··· 275 276 return 276 277 } 277 278 279 + meta := repositoryMeta{ 280 + DisplayName: getDisplayName(name), 281 + DirName: name, 282 + Description: getDescription(path), 283 + Ref: ref, 284 + } 285 + 286 + relpath := []string{} 287 + if len(treePath) > 0 { 288 + relpath = strings.Split(treePath, "/") 289 + } 290 + 291 + tpath := filepath.Join(d.c.Dirs.Templates, "*") 292 + t := template.Must(template.ParseGlob(tpath)) 293 + 294 + if r.URL.Query().Has("preview") { 295 + previewType := r.URL.Query().Get("preview") 296 + 297 + for _, renderer := range preview.GetPreviewRenderers(treePath) { 298 + resolvedPreviewType := renderer.GetPreviewType() 299 + 300 + if previewType != "" && resolvedPreviewType != previewType { 301 + continue 302 + } 303 + 304 + switch resolvedPreviewType { 305 + case "html": 306 + html, err := renderer.Render([]byte(contents)) 307 + if err != nil { 308 + log.Printf("Failed to render HTML preview: %s", err) 309 + d.Write500(w) 310 + return 311 + } 312 + 313 + data := repoBlobRefHTMLPreviewData{ 314 + Config: d.c, 315 + Meta: meta, 316 + Path: relpath, 317 + Content: template.HTML(html), 318 + } 319 + 320 + if err := t.ExecuteTemplate(w, "repo-blob-ref-html-preview", data); err != nil { 321 + log.Println(err) 322 + return 323 + } 324 + 325 + return 326 + } 327 + } 328 + 329 + log.Printf("Got ?preview=%s, but not preview renderer is available for the type", previewType) 330 + d.Write404(w) 331 + return 332 + } 333 + 278 334 lc, err := countLines(strings.NewReader(contents)) 279 335 if err != nil { 280 336 log.Printf("Failed to count lines for %s: %s", r.URL.Path, err) ··· 291 347 lines[i] = uint(i + 1) 292 348 } 293 349 294 - relpath := []string{} 295 - if len(treePath) > 0 { 296 - relpath = strings.Split(treePath, "/") 350 + renderers := preview.GetPreviewRenderers(treePath) 351 + previewTypes := make([]string, len(renderers)) 352 + for i, renderer := range renderers { 353 + previewTypes[i] = renderer.GetPreviewType() 297 354 } 298 355 299 356 data := repoBlobRefData{ 300 - Config: d.c, 301 - Meta: repositoryMeta{ 302 - DisplayName: getDisplayName(name), 303 - DirName: name, 304 - Description: getDescription(path), 305 - Ref: ref, 306 - }, 307 - Path: relpath, 308 - Content: contents, 309 - LineNumbers: lines, 357 + Config: d.c, 358 + Meta: meta, 359 + Path: relpath, 360 + Content: contents, 361 + LineNumbers: lines, 362 + PreviewTypes: previewTypes, 310 363 } 311 364 312 365 if d.c.Meta.SyntaxHighlight { ··· 317 370 data.SyntaxHighlightedContent = highlighted 318 371 } 319 372 } 320 - 321 - tpath := filepath.Join(d.c.Dirs.Templates, "*") 322 - t := template.Must(template.ParseGlob(tpath)) 323 373 324 374 if err := t.ExecuteTemplate(w, "repo-blob-ref", data); err != nil { 325 375 log.Println(err)
+147
static/html-content.css
··· 1 + /* Copyright 2025 Shota FUJI <pockawoooh@gmail.com> 2 + * SPDX-License-Identifier: MIT 3 + */ 4 + 5 + .html-content { 6 + line-height: 1.3; 7 + } 8 + 9 + .html-content > :first-child { 10 + margin-block-start: 0; 11 + } 12 + 13 + .html-content h1 { 14 + font-size: var(--font-xl); 15 + font-weight: var(--font-chonk); 16 + margin: var(--space-xxxl) 0; 17 + line-height: 1.2; 18 + } 19 + 20 + .html-content h2 { 21 + font-size: var(--font-lg); 22 + font-weight: var(--font-chonk); 23 + margin: var(--space-xxxl) 0; 24 + line-height: 1.2; 25 + } 26 + 27 + .html-content h3 { 28 + font-size: var(--font-md); 29 + font-weight: var(--font-chonk); 30 + margin: var(--space-xxxl) 0; 31 + margin-block-end: var(--space-xl); 32 + line-height: 1.2; 33 + } 34 + 35 + .html-content h4 { 36 + font-size: var(--font-md); 37 + font-weight: var(--font-thick); 38 + margin: var(--space-xxl) 0; 39 + line-height: 1.2; 40 + } 41 + 42 + .html-content h5 { 43 + font-size: var(--font-sm); 44 + font-weight: var(--font-chonk); 45 + margin: var(--space-xxl) 0; 46 + line-height: 1.2; 47 + } 48 + 49 + .html-content h6 { 50 + font-size: var(--font-sm); 51 + font-weight: var(--font-thick); 52 + margin: var(--space-xl) 0; 53 + line-height: 1.2; 54 + } 55 + 56 + .html-content p { 57 + font-size: var(--font-md); 58 + font-weight: var(--font-regular); 59 + margin: var(--space-lg) 0; 60 + } 61 + 62 + .html-content ul, .html-content ol { 63 + margin: var(--space-lg) 0; 64 + padding: 0; 65 + padding-inline-start: 1.2em; 66 + line-height: 1.2; 67 + } 68 + 69 + .html-content li + li { 70 + margin-block-start: var(--space-xxs); 71 + } 72 + 73 + .html-content pre { 74 + margin: var(--space-xl) 0; 75 + padding: var(--space-xxl) var(--space-xl); 76 + font-family: var(--font-mono); 77 + font-size: var(--font-sm); 78 + border: 1px solid var(--color-border-subtle); 79 + line-height: 1.2; 80 + 81 + border-radius: var(--radii-md); 82 + box-shadow: 1px 1px 4px var(--color-shadow); 83 + overflow-x: auto; 84 + overflow-y: hidden; 85 + } 86 + 87 + .html-content table { 88 + margin: 0; 89 + margin-block-end: var(--space-xxl); 90 + min-width: 100%; 91 + border-collapse: collapse; 92 + } 93 + 94 + .html-content thead > tr { 95 + border-block-end: 1px solid var(--color-border-subtle); 96 + } 97 + 98 + .html-content tbody > tr:first-of-type > td { 99 + padding-block-start: var(--space-md); 100 + } 101 + 102 + .html-content th { 103 + font-weight: var(--font-regular); 104 + color: var(--color-fg-weak); 105 + } 106 + 107 + .html-content th, td { 108 + font-size: var(--font-sm); 109 + padding: var(--space-xs) var(--space-md); 110 + } 111 + 112 + .html-content code:not(:where(pre > code)) { 113 + font-size: var(--font-sm); 114 + font-family: var(--font-mono); 115 + font-style: italic; 116 + 117 + color: var(--color-fg-weak); 118 + } 119 + 120 + .html-content a { 121 + text-decoration: underline; 122 + } 123 + 124 + .html-content a > code { 125 + color: inherit; 126 + } 127 + 128 + .html-content img[src^="./"] { 129 + display: inline-flex; 130 + flex-direction: column; 131 + padding: var(--space-lg) var(--space-xl); 132 + font-size: var(--font-sm); 133 + border: 1px solid var(--color-border-subtle); 134 + line-height: 1.2; 135 + gap: var(--space-xs); 136 + 137 + border-radius: var(--radii-md); 138 + box-shadow: 1px 1px 4px var(--color-shadow); 139 + } 140 + .html-content img[src^="./"]::after { 141 + display: block; 142 + content: "Rendering of local image is not supported."; 143 + font-size: var(--font-xs); 144 + font-style: italic; 145 + 146 + color: var(--color-fg-weak); 147 + }
+4
static/repo-blob-ref.css
··· 48 48 overflow-x: auto; 49 49 } 50 50 51 + .preview-type { 52 + text-transform: uppercase; 53 + } 54 + 51 55 /* 52 56 * https://github.com/alecthomas/chroma/blob/e0c774731c6f55889d36c4cbf18e7480e24c1020/types.go#L211 53 57 */
-144
static/repo-top.css
··· 20 20 padding: 0; 21 21 margin: 0; 22 22 } 23 - 24 - .readme { 25 - line-height: 1.3; 26 - } 27 - 28 - .readme > :first-child { 29 - margin-block-start: 0; 30 - } 31 - 32 - .readme h1 { 33 - font-size: var(--font-xl); 34 - font-weight: var(--font-chonk); 35 - margin: var(--space-xxxl) 0; 36 - line-height: 1.2; 37 - } 38 - 39 - .readme h2 { 40 - font-size: var(--font-lg); 41 - font-weight: var(--font-chonk); 42 - margin: var(--space-xxxl) 0; 43 - line-height: 1.2; 44 - } 45 - 46 - .readme h3 { 47 - font-size: var(--font-md); 48 - font-weight: var(--font-chonk); 49 - margin: var(--space-xxxl) 0; 50 - margin-block-end: var(--space-xl); 51 - line-height: 1.2; 52 - } 53 - 54 - .readme h4 { 55 - font-size: var(--font-md); 56 - font-weight: var(--font-thick); 57 - margin: var(--space-xxl) 0; 58 - line-height: 1.2; 59 - } 60 - 61 - .readme h5 { 62 - font-size: var(--font-sm); 63 - font-weight: var(--font-chonk); 64 - margin: var(--space-xxl) 0; 65 - line-height: 1.2; 66 - } 67 - 68 - .readme h6 { 69 - font-size: var(--font-sm); 70 - font-weight: var(--font-thick); 71 - margin: var(--space-xl) 0; 72 - line-height: 1.2; 73 - } 74 - 75 - .readme p { 76 - font-size: var(--font-md); 77 - font-weight: var(--font-regular); 78 - margin: var(--space-lg) 0; 79 - } 80 - 81 - .readme ul, .readme ol { 82 - margin: var(--space-lg) 0; 83 - padding: 0; 84 - padding-inline-start: 1.2em; 85 - line-height: 1.2; 86 - } 87 - 88 - .readme li + li { 89 - margin-block-start: var(--space-xxs); 90 - } 91 - 92 - .readme pre { 93 - margin: var(--space-xl) 0; 94 - padding: var(--space-xxl) var(--space-xl); 95 - font-family: var(--font-mono); 96 - font-size: var(--font-sm); 97 - border: 1px solid var(--color-border-subtle); 98 - line-height: 1.2; 99 - 100 - border-radius: var(--radii-md); 101 - box-shadow: 1px 1px 4px var(--color-shadow); 102 - overflow-x: auto; 103 - overflow-y: hidden; 104 - } 105 - 106 - .readme table { 107 - margin: 0; 108 - margin-block-end: var(--space-xxl); 109 - min-width: 100%; 110 - border-collapse: collapse; 111 - } 112 - 113 - .readme thead > tr { 114 - border-block-end: 1px solid var(--color-border-subtle); 115 - } 116 - 117 - .readme tbody > tr:first-of-type > td { 118 - padding-block-start: var(--space-md); 119 - } 120 - 121 - .readme th { 122 - font-weight: var(--font-regular); 123 - color: var(--color-fg-weak); 124 - } 125 - 126 - .readme th, td { 127 - font-size: var(--font-sm); 128 - padding: var(--space-xs) var(--space-md); 129 - } 130 - 131 - .readme code:not(:where(pre > code)) { 132 - font-size: var(--font-sm); 133 - font-family: var(--font-mono); 134 - font-style: italic; 135 - 136 - color: var(--color-fg-weak); 137 - } 138 - 139 - .readme a { 140 - text-decoration: underline; 141 - } 142 - 143 - .readme a > code { 144 - color: inherit; 145 - } 146 - 147 - .readme img[src^="./"] { 148 - display: inline-flex; 149 - flex-direction: column; 150 - padding: var(--space-lg) var(--space-xl); 151 - font-size: var(--font-sm); 152 - border: 1px solid var(--color-border-subtle); 153 - line-height: 1.2; 154 - gap: var(--space-xs); 155 - 156 - border-radius: var(--radii-md); 157 - box-shadow: 1px 1px 4px var(--color-shadow); 158 - } 159 - .readme img[src^="./"]::after { 160 - display: block; 161 - content: "Rendering of local image is not supported."; 162 - font-size: var(--font-xs); 163 - font-style: italic; 164 - 165 - color: var(--color-fg-weak); 166 - }
+76
templates/repo-blob-ref-html-preview.html.tmpl
··· 1 + <!-- 2 + Copyright 2025 Shota FUJI <pockawoooh@gmail.com> 3 + SPDX-License-Identifier: MIT 4 + --> 5 + {{ define "repo-blob-ref-html-preview" -}} 6 + <!DOCTYPE html> 7 + <html lang="en"> 8 + <head> 9 + <link rel="stylesheet" href="/static/html-content.css" /> 10 + {{ template "head" }} 11 + {{- $path := "" -}} 12 + {{- $name := "" -}} 13 + {{- range .Path -}} 14 + {{- $path = printf "%s/%s" $path . -}} 15 + {{- $name = . -}} 16 + {{- end -}} 17 + <title> 18 + {{ $path }} at {{ .Meta.Ref }} - {{ .Meta.DisplayName }} 19 + </title> 20 + <meta name="description" content='{{ $path }} in {{ .Meta.DisplayName }}' /> 21 + </head> 22 + <body> 23 + <header class="header"> 24 + <ol class="breadcrumbs"> 25 + <li> 26 + <a href="/">Top</a> 27 + </li> 28 + <li> 29 + <a href="/{{ .Meta.DirName }}">{{ .Meta.DisplayName }}</a> 30 + </li> 31 + <li> 32 + <a href="/{{ .Meta.DirName }}/tree/{{ .Meta.Ref }}">Files</a> 33 + </li> 34 + {{- $path_slice := .Path -}} 35 + {{- $meta := .Meta }} 36 + {{ range $i, $segment := .Path -}} 37 + <li> 38 + {{- $trail := "" -}} 39 + {{- range $j, $seg := $path_slice -}} 40 + {{- if le $j $i }} 41 + {{- $trail = printf "%s/%s" $trail $seg -}} 42 + {{- end -}} 43 + {{- end -}} 44 + {{- if eq $segment $name -}} 45 + <a href="/{{ $meta.DirName }}/blob/{{ $meta.Ref }}{{ $trail }}">{{ $segment }}</a> 46 + {{- else -}} 47 + <a href="/{{ $meta.DirName }}/tree/{{ $meta.Ref }}{{ $trail }}">{{ $segment }}</a> 48 + {{- end -}} 49 + </li> 50 + {{- end }} 51 + <li> 52 + <a href="/{{ .Meta.DirName }}/blob/{{ .Meta.Ref }}{{ $path }}?preview=html" aria-current="page"> 53 + Preview 54 + </a> 55 + </li> 56 + </ol> 57 + {{ template "repo-header" . }} 58 + {{ template "repo-nav" . }} 59 + </header> 60 + <main class="main"> 61 + <article class="html-content"> 62 + {{- .Content -}} 63 + </article> 64 + </main> 65 + <aside class="aside"> 66 + <dl class="metadata"> 67 + {{- template "tab-selector" -}} 68 + <dt class="metadata--key">Preview</dt> 69 + <dd class="metadata--value"> 70 + <a href="/{{ .Meta.DirName }}/blob/{{ .Meta.Ref }}{{ $path }}">View code</a> 71 + </dd> 72 + </dl> 73 + </aside> 74 + </body> 75 + </html> 76 + {{- end }}
+6
templates/repo-blob-ref.html.tmpl
··· 73 73 <aside class="aside"> 74 74 <dl class="metadata"> 75 75 {{- template "tab-selector" -}} 76 + {{- range .PreviewTypes -}} 77 + <dt class="metadata--key">Preview (<span class="preview-type">{{ . }}</span>)</dt> 78 + <dd class="metadata--value"> 79 + <a href="?preview={{ . }}">View in <span class="preview-type">{{ . }}</span></a> 80 + </dd> 81 + {{- end -}} 76 82 </dl> 77 83 </aside> 78 84 </body>
+2 -1
templates/repo-top.html.tmpl
··· 8 8 <head> 9 9 {{ template "head" }} 10 10 <link rel="stylesheet" href="/static/repo-top.css" /> 11 + <link rel="stylesheet" href="/static/html-content.css" /> 11 12 <title>{{ .Meta.DisplayName }} | {{ .Config.Meta.Title }}</title> 12 13 {{- if .Meta.Description }} 13 14 <meta name="description" content="{{ .Meta.Description }}" /> ··· 28 29 {{ template "repo-nav" . }} 29 30 </header> 30 31 <main class="main"> 31 - <article class="readme"> 32 + <article class="html-content"> 32 33 {{- .Readme -}} 33 34 </article> 34 35 </main>