clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

main
tinyrelay / internal / views / views.go
views.go 4.8K
1package views23import (4 "crypto/sha256"5 "encoding/hex"6 "html"7 "regexp"8 "strings"9)1011// Block is one fenced code block. Index counts every fenced block in the12// text from zero, whether or not it carries a language.13type Block struct {14 Index int `json:"index"`15 Lang string `json:"lang"`16 Source string `json:"source"`17}1819// NamePattern is the shape of a custom view name.20var NamePattern = regexp.MustCompile(`^[a-z0-9-]{1,32}$`)2122// OpenFence reports whether a line opens a fenced code block: three or more23// backticks or tildes, with the language as the first word after them. The24// marker returned is the fence run, so the closing line must use the same25// character.26func OpenFence(line string) (marker, lang string, ok bool) {27 trimmed := strings.TrimSpace(line)28 if len(trimmed) < 3 || (trimmed[0] != '`' && trimmed[0] != '~') {29 return "", "", false30 }31 run := len(trimmed) - len(strings.TrimLeft(trimmed, trimmed[:1]))32 if run < 3 {33 return "", "", false34 }35 info := strings.Fields(trimmed[run:])36 if len(info) > 0 {37 lang = strings.ToLower(info[0])38 }39 return trimmed[:run], lang, true40}4142// ClosesFence reports whether a line closes the fence opened with marker: a43// run of at least as many of the same character and nothing else.44func ClosesFence(line, marker string) bool {45 trimmed := strings.TrimSpace(line)46 return len(trimmed) >= len(marker) && strings.Trim(trimmed, marker[:1]) == ""47}4849// Blocks lists the fenced code blocks of a text in order. The source is the50// lines between the fences joined by newlines, without a trailing newline.51func Blocks(content string) []Block {52 lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")53 var out []Block54 for i := 0; i < len(lines); i++ {55 marker, lang, ok := OpenFence(lines[i])56 if !ok {57 continue58 }59 var body []string60 for i++; i < len(lines) && !ClosesFence(lines[i], marker); i++ {61 body = append(body, lines[i])62 }63 out = append(out, Block{Index: len(out), Lang: lang, Source: strings.Join(body, "\n")})64 }65 return out66}6768// Matching keeps the blocks whose language is one of the given languages.69func Matching(blocks []Block, languages []string) []Block {70 var out []Block71 for _, block := range blocks {72 if block.Lang != "" && contains(languages, block.Lang) {73 out = append(out, block)74 }75 }76 return out77}7879// Hash names a block: the SHA-256 of the language, a newline and the80// source, in hex. The same block in two texts has the same hash.81func Hash(lang, source string) string {82 sum := sha256.Sum256([]byte(lang + "\n" + source))83 return hex.EncodeToString(sum[:])84}8586// HashPattern is the shape of a block hash in a path.87var HashPattern = regexp.MustCompile(`^[0-9a-f]{64}$`)8889// Path is the address an artifact is served at.90func Path(view, hash, extension string) string {91 path := "/views/" + view + "/" + hash92 if extension != "" {93 path += "." + extension94 }95 return path96}9798// Figure is the markup for a block a custom view renders. The image remains99// transparent and follows the page color scheme, while the native details100// disclosure keeps the source available without requiring script.101func Figure(view, lang, source string) string {102 var b strings.Builder103 alt := "Rendered view"104 if lang != "" {105 alt = "Rendered " + lang + " block"106 }107 b.WriteString(`<figure data-view="` + html.EscapeString(view) + `"><view-artifact><img src="` + Path(view, Hash(lang, source), "") + `" alt="` + html.EscapeString(alt) + `"><details><summary>Source</summary><pre><code`)108 if lang != "" {109 b.WriteString(` data-lang="` + html.EscapeString(lang) + `"`)110 }111 b.WriteString(">" + html.EscapeString(source) + "</code></pre></details></view-artifact></figure>\n")112 return b.String()113}114115// Renderer decides how a fenced block is shown: the view that renders the116// language, if any. Languages maps a lowercase language to a view name.117type Renderer struct {118 Languages map[string]string119}120121// NewRenderer indexes views by the languages they render. The first view122// that names a language renders it.123func NewRenderer(views []View) Renderer {124 index := map[string]string{}125 for _, view := range views {126 for _, lang := range view.Languages {127 lang = strings.ToLower(lang)128 if _, taken := index[lang]; !taken {129 index[lang] = view.Name130 }131 }132 }133 return Renderer{Languages: index}134}135136// View is the summary a renderer needs: the name and the languages.137type View struct {138 Name string `json:"name"`139 Languages []string `json:"languages"`140}141142// Block returns the figure markup for a block when a view renders its143// language.144func (r Renderer) Block(lang, source string) (string, bool) {145 view, ok := r.Languages[strings.ToLower(lang)]146 if !ok || view == "" {147 return "", false148 }149 return Figure(view, strings.ToLower(lang), source), true150}151152func contains(values []string, value string) bool {153 for _, item := range values {154 if item == value {155 return true156 }157 }158 return false159}