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