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}