1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
// Package preview renders the current buffer through Glamour into a scrollable,
// read-only viewport โ the full glow read experience, markup concealed.
package preview
import (
"os"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glamour"
)
// Model wraps a Glamour renderer and a viewport.
type Model struct {
vp viewport.Model
style string
width int
height int
}
// New returns a preview using the given Glamour style (builtin name or a path
// to a style JSON file).
func New(style string) *Model {
return &Model{
vp: viewport.New(0, 0),
style: style,
width: 80,
height: 24,
}
}
// SetSize resizes the viewport.
func (m *Model) SetSize(w, h int) {
m.width = w
if h < 1 {
h = 1
}
m.height = h
m.vp.Width = w
m.vp.Height = h
}
// SetStyle changes the glamour style used by the next Render.
func (m *Model) SetStyle(s string) { m.style = s }
// Render runs markdown through Glamour and loads it into the viewport.
func (m *Model) Render(markdown string) error {
r, err := m.renderer()
if err != nil {
return err
}
out, err := r.Render(markdown)
if err != nil {
return err
}
m.vp.SetContent(out)
m.vp.GotoTop()
return nil
}
// knownStyles maps glamour's known builtin style names.
var knownStyles = map[string]bool{
"ascii": true,
"dark": true,
"light": true,
"dracula": true,
"tokyo-night": true,
"notty": true,
"pink": true,
}
// renderer builds a Glamour renderer, treating m.style as a file path when it
// exists on disk and as a builtin style name otherwise. Falls back to "dark" if
// the style is neither an existing file nor a known builtin name.
func (m *Model) renderer() (*glamour.TermRenderer, error) {
width := m.width
if width < 1 {
width = 80
}
opts := []glamour.TermRendererOption{glamour.WithWordWrap(width)}
// If it exists as a file, use it as a path
if _, err := os.Stat(m.style); err == nil {
opts = append(opts, glamour.WithStylePath(m.style))
} else if knownStyles[m.style] {
// If it's a known builtin name, use it
opts = append(opts, glamour.WithStandardStyle(m.style))
} else {
// Otherwise, fall back to "dark"
opts = append(opts, glamour.WithStandardStyle("dark"))
}
return glamour.NewTermRenderer(opts...)
}
// Update forwards scroll keys to the viewport.
func (m *Model) Update(msg tea.Msg) tea.Cmd {
var cmd tea.Cmd
m.vp, cmd = m.vp.Update(msg)
return cmd
}
// View renders the viewport.
func (m *Model) View() string { return m.vp.View() }
// Style returns the current glamour style (used in tests).
func (m *Model) Style() string { return m.style }
|