Changes
9 changed files (+360/-161)
-
-
@@ -121,6 +121,23 @@ SyntaxHighlightedContent template.HTML// LineNumbers holds sequential numbers starting from 1 up to line count of the blob. LineNumbers []uint // A list of preview output types. PreviewTypes []string } // repoBlobRefHTMLPreviewData is a data object passed to "repo-blob-ref-html-preview" template. type repoBlobRefHTMLPreviewData struct { // Config represents a resolved config based on "config.yaml". Config *config.Config Meta repositoryMeta // Path to the blob. Path []string // Rendered HTML Content template.HTML } // repoLogRefData is a data object passed to "repo-log-ref" template.
-
-
-
@@ -0,0 +1,42 @@// This file contains preview renderers. // // Copyright 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: MIT package preview import ( "path/filepath" "github.com/microcosm-cc/bluemonday" "github.com/russross/blackfriday/v2" ) type Renderer interface { GetPreviewType() string Render(code []byte) ([]byte, error) } type MarkdownToHtmlRenderer struct{} func (r MarkdownToHtmlRenderer) GetPreviewType() string { return "html" } func (r MarkdownToHtmlRenderer) Render(code []byte) ([]byte, error) { sanitizer := bluemonday.UGCPolicy() unsafe := blackfriday.Run(code, blackfriday.WithExtensions(blackfriday.CommonExtensions)) return sanitizer.SanitizeBytes(unsafe), nil } func GetPreviewRenderers(fileName string) []Renderer { ext := filepath.Ext(fileName) switch ext { case ".md", ".mkd", ".markdown": return []Renderer{MarkdownToHtmlRenderer{}} default: return []Renderer{} } }
-
-
-
@@ -17,6 +17,7 @@ "github.com/dustin/go-humanize""github.com/microcosm-cc/bluemonday" "github.com/pocka/legit/config" "github.com/pocka/legit/git" "github.com/pocka/legit/routes/preview" "github.com/russross/blackfriday/v2" )
-
@@ -275,6 +276,61 @@ w.Write([]byte(contents))return } meta := repositoryMeta{ DisplayName: getDisplayName(name), DirName: name, Description: getDescription(path), Ref: ref, } relpath := []string{} if len(treePath) > 0 { relpath = strings.Split(treePath, "/") } tpath := filepath.Join(d.c.Dirs.Templates, "*") t := template.Must(template.ParseGlob(tpath)) if r.URL.Query().Has("preview") { previewType := r.URL.Query().Get("preview") for _, renderer := range preview.GetPreviewRenderers(treePath) { resolvedPreviewType := renderer.GetPreviewType() if previewType != "" && resolvedPreviewType != previewType { continue } switch resolvedPreviewType { case "html": html, err := renderer.Render([]byte(contents)) if err != nil { log.Printf("Failed to render HTML preview: %s", err) d.Write500(w) return } data := repoBlobRefHTMLPreviewData{ Config: d.c, Meta: meta, Path: relpath, Content: template.HTML(html), } if err := t.ExecuteTemplate(w, "repo-blob-ref-html-preview", data); err != nil { log.Println(err) return } return } } log.Printf("Got ?preview=%s, but not preview renderer is available for the type", previewType) d.Write404(w) return } lc, err := countLines(strings.NewReader(contents)) if err != nil { log.Printf("Failed to count lines for %s: %s", r.URL.Path, err)
-
@@ -291,22 +347,19 @@lines[i] = uint(i + 1) } relpath := []string{} if len(treePath) > 0 { relpath = strings.Split(treePath, "/") renderers := preview.GetPreviewRenderers(treePath) previewTypes := make([]string, len(renderers)) for i, renderer := range renderers { previewTypes[i] = renderer.GetPreviewType() } data := repoBlobRefData{ Config: d.c, Meta: repositoryMeta{ DisplayName: getDisplayName(name), DirName: name, Description: getDescription(path), Ref: ref, }, Path: relpath, Content: contents, LineNumbers: lines, Config: d.c, Meta: meta, Path: relpath, Content: contents, LineNumbers: lines, PreviewTypes: previewTypes, } if d.c.Meta.SyntaxHighlight {
-
@@ -317,9 +370,6 @@ } else {data.SyntaxHighlightedContent = highlighted } } tpath := filepath.Join(d.c.Dirs.Templates, "*") t := template.Must(template.ParseGlob(tpath)) if err := t.ExecuteTemplate(w, "repo-blob-ref", data); err != nil { log.Println(err)
-
-
static/html-content.css (new)
-
@@ -0,0 +1,147 @@/* Copyright 2025 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: MIT */ .html-content { line-height: 1.3; } .html-content > :first-child { margin-block-start: 0; } .html-content h1 { font-size: var(--font-xl); font-weight: var(--font-chonk); margin: var(--space-xxxl) 0; line-height: 1.2; } .html-content h2 { font-size: var(--font-lg); font-weight: var(--font-chonk); margin: var(--space-xxxl) 0; line-height: 1.2; } .html-content h3 { font-size: var(--font-md); font-weight: var(--font-chonk); margin: var(--space-xxxl) 0; margin-block-end: var(--space-xl); line-height: 1.2; } .html-content h4 { font-size: var(--font-md); font-weight: var(--font-thick); margin: var(--space-xxl) 0; line-height: 1.2; } .html-content h5 { font-size: var(--font-sm); font-weight: var(--font-chonk); margin: var(--space-xxl) 0; line-height: 1.2; } .html-content h6 { font-size: var(--font-sm); font-weight: var(--font-thick); margin: var(--space-xl) 0; line-height: 1.2; } .html-content p { font-size: var(--font-md); font-weight: var(--font-regular); margin: var(--space-lg) 0; } .html-content ul, .html-content ol { margin: var(--space-lg) 0; padding: 0; padding-inline-start: 1.2em; line-height: 1.2; } .html-content li + li { margin-block-start: var(--space-xxs); } .html-content pre { margin: var(--space-xl) 0; padding: var(--space-xxl) var(--space-xl); font-family: var(--font-mono); font-size: var(--font-sm); border: 1px solid var(--color-border-subtle); line-height: 1.2; border-radius: var(--radii-md); box-shadow: 1px 1px 4px var(--color-shadow); overflow-x: auto; overflow-y: hidden; } .html-content table { margin: 0; margin-block-end: var(--space-xxl); min-width: 100%; border-collapse: collapse; } .html-content thead > tr { border-block-end: 1px solid var(--color-border-subtle); } .html-content tbody > tr:first-of-type > td { padding-block-start: var(--space-md); } .html-content th { font-weight: var(--font-regular); color: var(--color-fg-weak); } .html-content th, td { font-size: var(--font-sm); padding: var(--space-xs) var(--space-md); } .html-content code:not(:where(pre > code)) { font-size: var(--font-sm); font-family: var(--font-mono); font-style: italic; color: var(--color-fg-weak); } .html-content a { text-decoration: underline; } .html-content a > code { color: inherit; } .html-content img[src^="./"] { display: inline-flex; flex-direction: column; padding: var(--space-lg) var(--space-xl); font-size: var(--font-sm); border: 1px solid var(--color-border-subtle); line-height: 1.2; gap: var(--space-xs); border-radius: var(--radii-md); box-shadow: 1px 1px 4px var(--color-shadow); } .html-content img[src^="./"]::after { display: block; content: "Rendering of local image is not supported."; font-size: var(--font-xs); font-style: italic; color: var(--color-fg-weak); }
-
-
-
@@ -48,6 +48,10 @@ padding-inline-start: var(--space-sm);overflow-x: auto; } .preview-type { text-transform: uppercase; } /* * https://github.com/alecthomas/chroma/blob/e0c774731c6f55889d36c4cbf18e7480e24c1020/types.go#L211 */
-
-
-
@@ -20,147 +20,3 @@ list-style: none;padding: 0; margin: 0; } .readme { line-height: 1.3; } .readme > :first-child { margin-block-start: 0; } .readme h1 { font-size: var(--font-xl); font-weight: var(--font-chonk); margin: var(--space-xxxl) 0; line-height: 1.2; } .readme h2 { font-size: var(--font-lg); font-weight: var(--font-chonk); margin: var(--space-xxxl) 0; line-height: 1.2; } .readme h3 { font-size: var(--font-md); font-weight: var(--font-chonk); margin: var(--space-xxxl) 0; margin-block-end: var(--space-xl); line-height: 1.2; } .readme h4 { font-size: var(--font-md); font-weight: var(--font-thick); margin: var(--space-xxl) 0; line-height: 1.2; } .readme h5 { font-size: var(--font-sm); font-weight: var(--font-chonk); margin: var(--space-xxl) 0; line-height: 1.2; } .readme h6 { font-size: var(--font-sm); font-weight: var(--font-thick); margin: var(--space-xl) 0; line-height: 1.2; } .readme p { font-size: var(--font-md); font-weight: var(--font-regular); margin: var(--space-lg) 0; } .readme ul, .readme ol { margin: var(--space-lg) 0; padding: 0; padding-inline-start: 1.2em; line-height: 1.2; } .readme li + li { margin-block-start: var(--space-xxs); } .readme pre { margin: var(--space-xl) 0; padding: var(--space-xxl) var(--space-xl); font-family: var(--font-mono); font-size: var(--font-sm); border: 1px solid var(--color-border-subtle); line-height: 1.2; border-radius: var(--radii-md); box-shadow: 1px 1px 4px var(--color-shadow); overflow-x: auto; overflow-y: hidden; } .readme table { margin: 0; margin-block-end: var(--space-xxl); min-width: 100%; border-collapse: collapse; } .readme thead > tr { border-block-end: 1px solid var(--color-border-subtle); } .readme tbody > tr:first-of-type > td { padding-block-start: var(--space-md); } .readme th { font-weight: var(--font-regular); color: var(--color-fg-weak); } .readme th, td { font-size: var(--font-sm); padding: var(--space-xs) var(--space-md); } .readme code:not(:where(pre > code)) { font-size: var(--font-sm); font-family: var(--font-mono); font-style: italic; color: var(--color-fg-weak); } .readme a { text-decoration: underline; } .readme a > code { color: inherit; } .readme img[src^="./"] { display: inline-flex; flex-direction: column; padding: var(--space-lg) var(--space-xl); font-size: var(--font-sm); border: 1px solid var(--color-border-subtle); line-height: 1.2; gap: var(--space-xs); border-radius: var(--radii-md); box-shadow: 1px 1px 4px var(--color-shadow); } .readme img[src^="./"]::after { display: block; content: "Rendering of local image is not supported."; font-size: var(--font-xs); font-style: italic; color: var(--color-fg-weak); }
-
-
-
@@ -0,0 +1,76 @@<!-- Copyright 2025 Shota FUJI <pockawoooh@gmail.com> SPDX-License-Identifier: MIT --> {{ define "repo-blob-ref-html-preview" -}} <!DOCTYPE html> <html lang="en"> <head> <link rel="stylesheet" href="/static/html-content.css" /> {{ template "head" }} {{- $path := "" -}} {{- $name := "" -}} {{- range .Path -}} {{- $path = printf "%s/%s" $path . -}} {{- $name = . -}} {{- end -}} <title> {{ $path }} at {{ .Meta.Ref }} - {{ .Meta.DisplayName }} </title> <meta name="description" content='{{ $path }} in {{ .Meta.DisplayName }}' /> </head> <body> <header class="header"> <ol class="breadcrumbs"> <li> <a href="/">Top</a> </li> <li> <a href="/{{ .Meta.DirName }}">{{ .Meta.DisplayName }}</a> </li> <li> <a href="/{{ .Meta.DirName }}/tree/{{ .Meta.Ref }}">Files</a> </li> {{- $path_slice := .Path -}} {{- $meta := .Meta }} {{ range $i, $segment := .Path -}} <li> {{- $trail := "" -}} {{- range $j, $seg := $path_slice -}} {{- if le $j $i }} {{- $trail = printf "%s/%s" $trail $seg -}} {{- end -}} {{- end -}} {{- if eq $segment $name -}} <a href="/{{ $meta.DirName }}/blob/{{ $meta.Ref }}{{ $trail }}">{{ $segment }}</a> {{- else -}} <a href="/{{ $meta.DirName }}/tree/{{ $meta.Ref }}{{ $trail }}">{{ $segment }}</a> {{- end -}} </li> {{- end }} <li> <a href="/{{ .Meta.DirName }}/blob/{{ .Meta.Ref }}{{ $path }}?preview=html" aria-current="page"> Preview </a> </li> </ol> {{ template "repo-header" . }} {{ template "repo-nav" . }} </header> <main class="main"> <article class="html-content"> {{- .Content -}} </article> </main> <aside class="aside"> <dl class="metadata"> {{- template "tab-selector" -}} <dt class="metadata--key">Preview</dt> <dd class="metadata--value"> <a href="/{{ .Meta.DirName }}/blob/{{ .Meta.Ref }}{{ $path }}">View code</a> </dd> </dl> </aside> </body> </html> {{- end }}
-
-
-
@@ -73,6 +73,12 @@ </main><aside class="aside"> <dl class="metadata"> {{- template "tab-selector" -}} {{- range .PreviewTypes -}} <dt class="metadata--key">Preview (<span class="preview-type">{{ . }}</span>)</dt> <dd class="metadata--value"> <a href="?preview={{ . }}">View in <span class="preview-type">{{ . }}</span></a> </dd> {{- end -}} </dl> </aside> </body>
-
-
-
@@ -8,6 +8,7 @@ <html lang="en"><head> {{ template "head" }} <link rel="stylesheet" href="/static/repo-top.css" /> <link rel="stylesheet" href="/static/html-content.css" /> <title>{{ .Meta.DisplayName }} | {{ .Config.Meta.Title }}</title> {{- if .Meta.Description }} <meta name="description" content="{{ .Meta.Description }}" />
-
@@ -28,7 +29,7 @@ {{ template "repo-header" . }}{{ template "repo-nav" . }} </header> <main class="main"> <article class="readme"> <article class="html-content"> {{- .Readme -}} </article> </main>
-