tinyrelayfile
tinyrelay/internal/views/views.go/

internal/views/views.go raw
1package views
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "html"
7 "regexp"
8 "strings"
9)
10
11// Block is one fenced code block. Index counts every fenced block in the
12// 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}
18
19// NamePattern is the shape of a custom view name.
20var NamePattern = regexp.MustCompile(`^[a-z0-9-]{1,32}$`)
21
22// OpenFence reports whether a line opens a fenced code block: three or more
23// backticks or tildes, with the language as the first word after them. The
24// marker returned is the fence run, so the closing line must use the same
25// 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 "", "", false
30 }
31 run := len(trimmed) - len(strings.TrimLeft(trimmed, trimmed[:1]))
32 if run < 3 {
33 return "", "", false
34 }
35 info := strings.Fields(trimmed[run:])
36 if len(info) > 0 {
37 lang = strings.ToLower(info[0])
38 }
39 return trimmed[:run], lang, true
40}
41
42// ClosesFence reports whether a line closes the fence opened with marker: a
43// 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}
48
49// Blocks lists the fenced code blocks of a text in order. The source is the
50// 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 []Block
54 for i := 0; i < len(lines); i++ {
55 marker, lang, ok := OpenFence(lines[i])
56 if !ok {
57 continue
58 }
59 var body []string
60 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 out
66}
67
68// Matching keeps the blocks whose language is one of the given languages.
69func Matching(blocks []Block, languages []string) []Block {
70 var out []Block
71 for _, block := range blocks {
72 if block.Lang != "" && contains(languages, block.Lang) {
73 out = append(out, block)
74 }
75 }
76 return out
77}
78
79// Hash names a block: the SHA-256 of the language, a newline and the
80// 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}
85
86// HashPattern is the shape of a block hash in a path.
87var HashPattern = regexp.MustCompile(`^[0-9a-f]{64}$`)
88
89// Path is the address an artifact is served at.
90func Path(view, hash, extension string) string {
91 path := "/views/" + view + "/" + hash
92 if extension != "" {
93 path += "." + extension
94 }
95 return path
96}
97
98// Figure is the markup for a block a custom view renders. The image remains
99// transparent and follows the page color scheme, while the native details
100// disclosure keeps the source available without requiring script.
101func Figure(view, lang, source string) string {
102 var b strings.Builder
103 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}
114
115// Renderer decides how a fenced block is shown: the view that renders the
116// language, if any. Languages maps a lowercase language to a view name.
117type Renderer struct {
118 Languages map[string]string
119}
120
121// NewRenderer indexes views by the languages they render. The first view
122// 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.Name
130 }
131 }
132 }
133 return Renderer{Languages: index}
134}
135
136// 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}
141
142// Block returns the figure markup for a block when a view renders its
143// language.
144func (r Renderer) Block(lang, source string) (string, bool) {
145 view, ok := r.Languages[strings.ToLower(lang)]
146 if !ok || view == "" {
147 return "", false
148 }
149 return Figure(view, strings.ToLower(lang), source), true
150}
151
152func contains(values []string, value string) bool {
153 for _, item := range values {
154 if item == value {
155 return true
156 }
157 }
158 return false
159}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs