/
templates.go
210 lines (183 loc) · 5.56 KB
/
templates.go
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package markup
import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"github.com/niklasfasching/go-org/org"
"github.com/osteele/liquid"
"github.com/yuin/goldmark"
gm_highlight "github.com/yuin/goldmark-highlighting/v2"
"gopkg.in/yaml.v3"
)
const FM_SEPARATOR = "---"
const NO_SYNTAX_HIGHLIGHTING = ""
const CODE_TABWIDTH = 4
type Engine = liquid.Engine
type Template struct {
SrcPath string
Metadata map[string]interface{}
liquidTemplate liquid.Template
}
// Create a new template engine, with custom liquid filters.
// The `siteUrl` is necessary to provide context for the absolute_url filter.
func NewEngine(siteUrl string, includesDir string) *Engine {
e := liquid.NewEngine()
loadJekyllFilters(e, siteUrl, includesDir)
return e
}
// Try to parse a liquid template at the given location.
// Files starting with front matter (--- sorrrounded yaml)
// are considered templates. If the given file is not headed by front matter
// return (nil, nil).
// The front matter contents are stored in the returned template's Metadata.
func Parse(engine *Engine, path string) (*Template, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Scan()
line := scanner.Text()
// if the file doesn't start with a front matter delimiter, it's not a template
if strings.TrimSpace(line) != FM_SEPARATOR {
return nil, nil
}
// extract the yaml front matter and save the rest of the template content separately
var yamlContent []byte
var liquidContent []byte
yamlClosed := false
for scanner.Scan() {
line := append(scanner.Bytes(), '\n')
if yamlClosed {
liquidContent = append(liquidContent, line...)
} else {
if strings.TrimSpace(scanner.Text()) == FM_SEPARATOR {
yamlClosed = true
continue
}
yamlContent = append(yamlContent, line...)
}
}
liquidContent = bytes.TrimSuffix(liquidContent, []byte("\n"))
if !yamlClosed {
return nil, errors.New("front matter not closed")
}
metadata := make(map[string]interface{})
if len(yamlContent) != 0 {
err := yaml.Unmarshal([]byte(yamlContent), &metadata)
if err != nil {
return nil, fmt.Errorf("invalid yaml: %s", err)
}
}
liquid, err := engine.ParseTemplateAndCache(liquidContent, path, 0)
if err != nil {
return nil, err
}
templ := Template{SrcPath: path, Metadata: metadata, liquidTemplate: *liquid}
return &templ, nil
}
// Return the extension of this template's source file.
func (templ Template) SrcExt() string {
return filepath.Ext(templ.SrcPath)
}
// Return the extension for the output format of this template
func (templ Template) TargetExt() string {
ext := filepath.Ext(templ.SrcPath)
if ext == ".org" || ext == ".md" {
return ".html"
}
return ext
}
func (templ Template) IsDraft() bool {
if draft, ok := templ.Metadata["draft"]; ok {
return draft.(bool)
}
return false
}
func (templ Template) IsPost() bool {
_, ok := templ.Metadata["date"]
return ok
}
// Renders the liquid template with default bindings.
func (templ Template) Render() ([]byte, error) {
ctx := map[string]interface{}{
"page": templ.Metadata,
}
return templ.RenderWith(ctx, NO_SYNTAX_HIGHLIGHTING)
}
// Renders the liquid template with the given context as bindings.
// If the template source is org or md, convert them to html after the
// liquid rendering.
func (templ Template) RenderWith(context map[string]interface{}, hlTheme string) ([]byte, error) {
// liquid rendering
content, err := templ.liquidTemplate.Render(context)
if err != nil {
return nil, err
}
if templ.SrcExt() == ".org" {
// org-mode rendering
doc := org.New().Parse(bytes.NewReader(content), templ.SrcPath)
htmlWriter := org.NewHTMLWriter()
// make * -> h1, ** -> h2, etc
htmlWriter.TopLevelHLevel = 1
if hlTheme != NO_SYNTAX_HIGHLIGHTING {
htmlWriter.HighlightCodeBlock = highlightCodeBlock(hlTheme)
}
contentStr, err := doc.Write(htmlWriter)
if err != nil {
return nil, err
}
content = []byte(contentStr)
} else if templ.SrcExt() == ".md" {
// markdown rendering
var buf bytes.Buffer
options := make([]goldmark.Option, 0)
if hlTheme != NO_SYNTAX_HIGHLIGHTING {
options = append(options, goldmark.WithExtensions(gm_highlight.NewHighlighting(
gm_highlight.WithStyle(hlTheme),
gm_highlight.WithFormatOptions(html.TabWidth(CODE_TABWIDTH)),
)))
}
md := goldmark.New(options...)
if err := md.Convert(content, &buf); err != nil {
return nil, err
}
content = buf.Bytes()
}
return content, nil
}
func highlightCodeBlock(hlTheme string) func(source string, lang string, inline bool, params map[string]string) string {
// from https://github.com/niklasfasching/go-org/blob/a32df1461eb34a451b1e0dab71bd9b2558ea5dc4/blorg/util.go#L58
return func(source, lang string, inline bool, params map[string]string) string {
var w strings.Builder
l := lexers.Get(lang)
if l == nil {
l = lexers.Fallback
}
l = chroma.Coalesce(l)
it, _ := l.Tokenise(nil, source)
options := []html.Option{
html.TabWidth(CODE_TABWIDTH),
}
if params[":hl_lines"] != "" {
ranges := org.ParseRanges(params[":hl_lines"])
if ranges != nil {
options = append(options, html.HighlightLines(ranges))
}
}
_ = html.New(options...).Format(&w, styles.Get(hlTheme), it)
if inline {
return `<div class="highlight-inline">` + "\n" + w.String() + "\n" + `</div>`
}
return `<div class="highlight">` + "\n" + w.String() + "\n" + `</div>`
}
}