|
7 | 7 | "sort"
|
8 | 8 | "strconv"
|
9 | 9 | "strings"
|
| 10 | + "sync" |
10 | 11 |
|
11 | 12 | "github.com/alecthomas/chroma/v2"
|
12 | 13 | )
|
@@ -133,6 +134,7 @@ func New(options ...Option) *Formatter {
|
133 | 134 | baseLineNumber: 1,
|
134 | 135 | preWrapper: defaultPreWrapper,
|
135 | 136 | }
|
| 137 | + f.styleCache = newStyleCache(f) |
136 | 138 | for _, option := range options {
|
137 | 139 | option(f)
|
138 | 140 | }
|
@@ -189,6 +191,7 @@ var (
|
189 | 191 |
|
190 | 192 | // Formatter that generates HTML.
|
191 | 193 | type Formatter struct {
|
| 194 | + styleCache *styleCache |
192 | 195 | standalone bool
|
193 | 196 | prefix string
|
194 | 197 | Classes bool // Exported field to detect when classes are being used
|
@@ -221,12 +224,7 @@ func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Ite
|
221 | 224 | //
|
222 | 225 | // OTOH we need to be super careful about correct escaping...
|
223 | 226 | func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.Token) (err error) { // nolint: gocyclo
|
224 |
| - css := f.styleToCSS(style) |
225 |
| - if !f.Classes { |
226 |
| - for t, style := range css { |
227 |
| - css[t] = compressStyle(style) |
228 |
| - } |
229 |
| - } |
| 227 | + css := f.styleCache.get(style) |
230 | 228 | if f.standalone {
|
231 | 229 | fmt.Fprint(w, "<html>\n")
|
232 | 230 | if f.Classes {
|
@@ -420,7 +418,7 @@ func (f *Formatter) tabWidthStyle() string {
|
420 | 418 |
|
421 | 419 | // WriteCSS writes CSS style definitions (without any surrounding HTML).
|
422 | 420 | func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
|
423 |
| - css := f.styleToCSS(style) |
| 421 | + css := f.styleCache.get(style) |
424 | 422 | // Special-case background as it is mapped to the outer ".chroma" class.
|
425 | 423 | if _, err := fmt.Fprintf(w, "/* %s */ .%sbg { %s }\n", chroma.Background, f.prefix, css[chroma.Background]); err != nil {
|
426 | 424 | return err
|
@@ -563,3 +561,60 @@ func compressStyle(s string) string {
|
563 | 561 | }
|
564 | 562 | return strings.Join(out, ";")
|
565 | 563 | }
|
| 564 | + |
| 565 | +const styleCacheLimit = 16 |
| 566 | + |
| 567 | +type styleCacheEntry struct { |
| 568 | + style *chroma.Style |
| 569 | + cache map[chroma.TokenType]string |
| 570 | +} |
| 571 | + |
| 572 | +type styleCache struct { |
| 573 | + mu sync.Mutex |
| 574 | + // LRU cache of compiled (and possibly compressed) styles. This is a slice |
| 575 | + // because the cache size is small, and a slice is sufficiently fast for |
| 576 | + // small N. |
| 577 | + cache []styleCacheEntry |
| 578 | + f *Formatter |
| 579 | +} |
| 580 | + |
| 581 | +func newStyleCache(f *Formatter) *styleCache { |
| 582 | + return &styleCache{f: f} |
| 583 | +} |
| 584 | + |
| 585 | +func (l *styleCache) get(style *chroma.Style) map[chroma.TokenType]string { |
| 586 | + l.mu.Lock() |
| 587 | + defer l.mu.Unlock() |
| 588 | + |
| 589 | + // Look for an existing entry. |
| 590 | + for i := len(l.cache) - 1; i >= 0; i-- { |
| 591 | + entry := l.cache[i] |
| 592 | + if entry.style == style { |
| 593 | + // Top of the cache, no need to adjust the order. |
| 594 | + if i == len(l.cache)-1 { |
| 595 | + return entry.cache |
| 596 | + } |
| 597 | + // Move this entry to the end of the LRU |
| 598 | + copy(l.cache[i:], l.cache[i+1:]) |
| 599 | + l.cache[len(l.cache)-1] = entry |
| 600 | + return entry.cache |
| 601 | + } |
| 602 | + } |
| 603 | + |
| 604 | + // No entry, create one. |
| 605 | + cached := l.f.styleToCSS(style) |
| 606 | + if !l.f.Classes { |
| 607 | + for t, style := range cached { |
| 608 | + cached[t] = compressStyle(style) |
| 609 | + } |
| 610 | + } |
| 611 | + for t, style := range cached { |
| 612 | + cached[t] = compressStyle(style) |
| 613 | + } |
| 614 | + // Evict the oldest entry. |
| 615 | + if len(l.cache) >= styleCacheLimit { |
| 616 | + l.cache = l.cache[0:copy(l.cache, l.cache[1:])] |
| 617 | + } |
| 618 | + l.cache = append(l.cache, styleCacheEntry{style: style, cache: cached}) |
| 619 | + return cached |
| 620 | +} |
0 commit comments