master
1package main
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "html/template"
8 "os"
9 "path/filepath"
10 "runtime"
11 "strings"
12 "sync"
13
14 "github.com/alecthomas/chroma/v2/formatters/html"
15 "github.com/alecthomas/chroma/v2/lexers"
16 "github.com/alecthomas/chroma/v2/styles"
17
18 "github.com/antonmedv/gitmal/pkg/git"
19 "github.com/antonmedv/gitmal/pkg/links"
20 "github.com/antonmedv/gitmal/pkg/progress_bar"
21 "github.com/antonmedv/gitmal/pkg/templates"
22)
23
24func generateBlobs(files []git.Blob, params Params) error {
25 // Prepare shared, read-only resources
26 var css strings.Builder
27 style := styles.Get(params.Style)
28 if style == nil {
29 return fmt.Errorf("unknown style: %s", params.Style)
30 }
31
32 formatterOptions := []html.Option{
33 html.WithLineNumbers(true),
34 html.WithLinkableLineNumbers(true, "L"),
35 html.WithClasses(true),
36 html.WithCSSComments(false),
37 }
38
39 // Use a temporary formatter to render CSS once
40 if err := html.New(formatterOptions...).WriteCSS(&css, style); err != nil {
41 return err
42 }
43
44 dirsSet := links.BuildDirSet(files)
45 filesSet := links.BuildFileSet(files)
46
47 // Bounded worker pool
48 workers := runtime.NumCPU()
49 if workers < 1 {
50 workers = 1
51 }
52
53 ctx, cancel := context.WithCancel(context.Background())
54 defer cancel()
55
56 jobs := make(chan git.Blob)
57 errCh := make(chan error, 1)
58 var wg sync.WaitGroup
59
60 p := progress_bar.NewProgressBar("blobs for "+params.Ref.String(), len(files))
61
62 workerFn := func() {
63 defer wg.Done()
64
65 // Per-worker instances
66 md := createMarkdown(params.Style)
67 formatter := html.New(formatterOptions...)
68
69 check := func(err error) bool {
70 if err != nil {
71 select {
72 case errCh <- err:
73 cancel()
74 default:
75 }
76 return true
77 }
78 return false
79 }
80
81 for {
82 select {
83 case <-ctx.Done():
84 return
85 case blob, ok := <-jobs:
86 if !ok {
87 return
88 }
89 func() {
90 var content string
91 data, isBin, err := git.BlobContent(params.Ref, blob.Path, params.RepoDir)
92 if check(err) {
93 return
94 }
95
96 isImg := isImage(blob.Path)
97 if !isBin {
98 content = string(data)
99 }
100
101 outPath := filepath.Join(params.OutputDir, "blob", params.Ref.DirName(), blob.Path) + ".html"
102 if err := os.MkdirAll(filepath.Dir(outPath), 0o755); check(err) {
103 return
104 }
105
106 f, err := os.Create(outPath)
107 if check(err) {
108 return
109 }
110 defer func() {
111 _ = f.Close()
112 }()
113
114 depth := 0
115 if strings.Contains(blob.Path, "/") {
116 depth = len(strings.Split(blob.Path, "/")) - 1
117 }
118 rootHref := strings.Repeat("../", depth+2)
119
120 rawPath := filepath.Join(params.OutputDir, "raw", params.Ref.DirName(), blob.Path)
121 if err := os.MkdirAll(filepath.Dir(rawPath), 0o755); check(err) {
122 return
123 }
124
125 rf, err := os.Create(rawPath)
126 if check(err) {
127 return
128 }
129
130 if _, err := rf.Write(data); check(err) {
131 return
132 }
133 _ = rf.Close()
134
135 rawHref := filepath.Join(rootHref, "raw", params.Ref.DirName(), blob.Path)
136
137 if isMarkdown(blob.Path) {
138 var b bytes.Buffer
139 if err := md.Convert([]byte(content), &b); check(err) {
140 return
141 }
142
143 contentHTML := links.Resolve(
144 b.String(),
145 blob.Path,
146 rootHref,
147 params.Ref.DirName(),
148 dirsSet,
149 filesSet,
150 )
151
152 err = templates.MarkdownTemplate.ExecuteTemplate(f, "layout.gohtml", templates.MarkdownParams{
153 LayoutParams: templates.LayoutParams{
154 Title: fmt.Sprintf("%s/%s at %s", params.Name, blob.Path, params.Ref),
155 Dark: params.Dark,
156 CSSMarkdown: cssMarkdown(params.Dark),
157 Name: params.Name,
158 SiteName: params.SiteName,
159 RootHref: rootHref + params.RootPrefix,
160 RepoHref: rootHref,
161 CurrentRefDir: params.Ref.DirName(),
162 Selected: "code",
163 InlineStyles: params.InlineStyles,
164 },
165 HeaderParams: templates.HeaderParams{
166 Ref: params.Ref,
167 Breadcrumbs: breadcrumbs(params.Name, blob.Path, true),
168 RawHref: rawHref,
169 },
170 Blob: blob,
171 Content: template.HTML(contentHTML),
172 })
173 if check(err) {
174 return
175 }
176
177 } else {
178
179 var contentHTML template.HTML
180 if !isBin {
181 var b bytes.Buffer
182 lx := lexers.Match(blob.Path)
183 if lx == nil {
184 lx = lexers.Fallback
185 }
186 iterator, _ := lx.Tokenise(nil, content)
187 if err := formatter.Format(&b, style, iterator); check(err) {
188 return
189 }
190 contentHTML = template.HTML(b.String())
191
192 }
193
194 if isImg {
195 contentHTML = template.HTML(fmt.Sprintf(`<img src="%s" alt="%s" />`, rawHref, blob.FileName))
196 }
197
198 err = templates.BlobTemplate.ExecuteTemplate(f, "layout.gohtml", templates.BlobParams{
199 LayoutParams: templates.LayoutParams{
200 Title: fmt.Sprintf("%s/%s at %s", params.Name, blob.Path, params.Ref),
201 Dark: params.Dark,
202 Name: params.Name,
203 SiteName: params.SiteName,
204 RootHref: rootHref + params.RootPrefix,
205 RepoHref: rootHref,
206 CurrentRefDir: params.Ref.DirName(),
207 Selected: "code",
208 InlineStyles: params.InlineStyles,
209 },
210 HeaderParams: templates.HeaderParams{
211 Ref: params.Ref,
212 Breadcrumbs: breadcrumbs(params.Name, blob.Path, true),
213 RawHref: rawHref,
214 },
215 CSS: template.CSS(css.String()),
216 Blob: blob,
217 IsBinary: isBin,
218 IsImage: isImg,
219 Content: contentHTML,
220 })
221 if check(err) {
222 return
223 }
224 }
225 }()
226
227 p.Inc()
228 }
229 }
230 }
231
232 // Start workers
233 wg.Add(workers)
234 for i := 0; i < workers; i++ {
235 go workerFn()
236 }
237
238 // Feed jobs
239 go func() {
240 defer close(jobs)
241 for _, b := range files {
242 select {
243 case <-ctx.Done():
244 return
245 case jobs <- b:
246 }
247 }
248 }()
249
250 // Wait for workers
251 doneCh := make(chan struct{})
252 go func() {
253 wg.Wait()
254 close(doneCh)
255 }()
256
257 var runErr error
258 select {
259 case runErr = <-errCh:
260 // error occurred, wait workers to finish
261 <-doneCh
262 case <-doneCh:
263 }
264
265 p.Done()
266 return runErr
267}