appview/pages: add templates to render strings #413

merged
opened by oppi.li targeting master from push-potvrpwlpwsl
Changed files
+159
appview
pages
templates
strings
+74
appview/pages/pages.go
··· 31 31 chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 32 32 "github.com/alecthomas/chroma/v2/lexers" 33 33 "github.com/alecthomas/chroma/v2/styles" 34 + "github.com/bluesky-social/indigo/atproto/identity" 34 35 "github.com/bluesky-social/indigo/atproto/syntax" 35 36 "github.com/go-git/go-git/v5/plumbing" 36 37 "github.com/go-git/go-git/v5/plumbing/object" ··· 1142 1143 return p.executeRepo("repo/pipelines/workflow", w, params) 1143 1144 } 1144 1145 1146 + type PutStringParams struct { 1147 + LoggedInUser *oauth.User 1148 + Action string 1149 + 1150 + // this is supplied in the case of editing an existing string 1151 + String db.String 1152 + } 1153 + 1154 + func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1155 + return p.execute("strings/put", w, params) 1156 + } 1157 + 1158 + type StringsDashboardParams struct { 1159 + LoggedInUser *oauth.User 1160 + Card ProfileCard 1161 + Strings []db.String 1162 + } 1163 + 1164 + func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1165 + return p.execute("strings/dashboard", w, params) 1166 + } 1167 + 1168 + type SingleStringParams struct { 1169 + LoggedInUser *oauth.User 1170 + ShowRendered bool 1171 + RenderToggle bool 1172 + RenderedContents template.HTML 1173 + String db.String 1174 + Stats db.StringStats 1175 + Owner identity.Identity 1176 + } 1177 + 1178 + func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1179 + var style *chroma.Style = styles.Get("catpuccin-latte") 1180 + 1181 + if params.ShowRendered { 1182 + switch markup.GetFormat(params.String.Filename) { 1183 + case markup.FormatMarkdown: 1184 + p.rctx.RendererType = markup.RendererTypeDefault 1185 + htmlString := p.rctx.RenderMarkdown(params.String.Contents) 1186 + params.RenderedContents = template.HTML(p.rctx.Sanitize(htmlString)) 1187 + } 1188 + } 1189 + 1190 + c := params.String.Contents 1191 + formatter := chromahtml.New( 1192 + chromahtml.InlineCode(false), 1193 + chromahtml.WithLineNumbers(true), 1194 + chromahtml.WithLinkableLineNumbers(true, "L"), 1195 + chromahtml.Standalone(false), 1196 + chromahtml.WithClasses(true), 1197 + ) 1198 + 1199 + lexer := lexers.Get(filepath.Base(params.String.Filename)) 1200 + if lexer == nil { 1201 + lexer = lexers.Fallback 1202 + } 1203 + 1204 + iterator, err := lexer.Tokenise(nil, c) 1205 + if err != nil { 1206 + return fmt.Errorf("chroma tokenize: %w", err) 1207 + } 1208 + 1209 + var code bytes.Buffer 1210 + err = formatter.Format(&code, style, iterator) 1211 + if err != nil { 1212 + return fmt.Errorf("chroma format: %w", err) 1213 + } 1214 + 1215 + params.String.Contents = code.String() 1216 + return p.execute("strings/string", w, params) 1217 + } 1218 + 1145 1219 func (p *Pages) Static() http.Handler { 1146 1220 if p.dev { 1147 1221 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
+85
appview/pages/templates/strings/string.html
··· 1 + {{ define "title" }}{{ .String.Filename }} · by {{ didOrHandle .Owner.DID.String .Owner.Handle.String }}{{ end }} 2 + 3 + {{ define "extrameta" }} 4 + {{ $ownerId := didOrHandle .Owner.DID.String .Owner.Handle.String }} 5 + <meta property="og:title" content="{{ .String.Filename }} · by {{ $ownerId }}" /> 6 + <meta property="og:type" content="object" /> 7 + <meta property="og:url" content="https://tangled.sh/strings/{{ $ownerId }}/{{ .String.Rkey }}" /> 8 + <meta property="og:description" content="{{ .String.Description }}" /> 9 + {{ end }} 10 + 11 + {{ define "topbar" }} 12 + {{ template "layouts/topbar" $ }} 13 + {{ end }} 14 + 15 + {{ define "content" }} 16 + {{ $ownerId := didOrHandle .Owner.DID.String .Owner.Handle.String }} 17 + <section id="string-header" class="mb-4 py-2 px-6 dark:text-white"> 18 + <div class="text-lg flex items-center justify-between"> 19 + <div> 20 + <a href="/strings/{{ $ownerId }}">{{ $ownerId }}</a> 21 + <span class="select-none">/</span> 22 + <a href="/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a> 23 + </div> 24 + {{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }} 25 + <div class="flex gap-2 text-base"> 26 + <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 27 + hx-boost="true" 28 + href="/strings/{{ .String.Did }}/{{ .String.Rkey }}/edit"> 29 + {{ i "pencil" "size-4" }} 30 + <span class="hidden md:inline">edit</span> 31 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 32 + </a> 33 + <button 34 + class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group p-2" 35 + title="Delete string" 36 + hx-delete="/strings/{{ .String.Did }}/{{ .String.Rkey }}/" 37 + hx-swap="none" 38 + hx-confirm="Are you sure you want to delete the gist `{{ .String.Filename }}`?" 39 + > 40 + {{ i "trash-2" "size-4" }} 41 + <span class="hidden md:inline">delete</span> 42 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 43 + </button> 44 + </div> 45 + {{ end }} 46 + </div> 47 + <span class="flex items-center"> 48 + {{ with .String.Description }} 49 + {{ . }} 50 + <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 51 + {{ end }} 52 + 53 + {{ with .String.Edited }} 54 + <span>edited {{ template "repo/fragments/shortTimeAgo" . }}</span> 55 + {{ else }} 56 + {{ template "repo/fragments/shortTimeAgo" .String.Created }} 57 + {{ end }} 58 + </span> 59 + </section> 60 + <section class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full dark:text-white"> 61 + <div class="flex justify-between items-center text-gray-500 dark:text-gray-400 text-sm md:text-base pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700"> 62 + <span>{{ .String.Filename }}</span> 63 + <div> 64 + <span>{{ .Stats.LineCount }} lines</span> 65 + <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 66 + <span>{{ byteFmt .Stats.ByteCount }}</span> 67 + <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 68 + <a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}/raw">view raw</a> 69 + {{ if .RenderToggle }} 70 + <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 71 + <a href="?code={{ .ShowRendered }}" hx-boost="true"> 72 + view {{ if .ShowRendered }}code{{ else }}rendered{{ end }} 73 + </a> 74 + {{ end }} 75 + </div> 76 + </div> 77 + <div class="overflow-auto relative"> 78 + {{ if .ShowRendered }} 79 + <div id="blob-contents" class="prose dark:prose-invert">{{ .RenderedContents }}</div> 80 + {{ else }} 81 + <div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ .String.Contents | escapeHtml }}</div> 82 + {{ end }} 83 + </div> 84 + </section> 85 + {{ end }}