aboutsummaryrefslogtreecommitdiffhomepage
path: root/modules/caddyhttp/templates/frontmatter.go
blob: fb62a41847f28d81d9e1bb56fa90b843dc10657e (plain)
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
package templates

import (
	"encoding/json"
	"fmt"
	"strings"
	"unicode"

	"github.com/BurntSushi/toml"
	"gopkg.in/yaml.v3"
)

func extractFrontMatter(input string) (map[string]any, string, error) {
	// get the bounds of the first non-empty line
	var firstLineStart, firstLineEnd int
	lineEmpty := true
	for i, b := range input {
		if b == '\n' {
			firstLineStart = firstLineEnd
			if firstLineStart > 0 {
				firstLineStart++ // skip newline character
			}
			firstLineEnd = i
			if !lineEmpty {
				break
			}
			continue
		}
		lineEmpty = lineEmpty && unicode.IsSpace(b)
	}
	firstLine := input[firstLineStart:firstLineEnd]

	// ensure residue windows carriage return byte is removed
	firstLine = strings.TrimSpace(firstLine)

	// see what kind of front matter there is, if any
	var closingFence []string
	var fmParser func([]byte) (map[string]any, error)
	for _, fmType := range supportedFrontMatterTypes {
		if firstLine == fmType.FenceOpen {
			closingFence = fmType.FenceClose
			fmParser = fmType.ParseFunc
			break
		}
	}

	if fmParser == nil {
		// no recognized front matter; whole document is body
		return nil, input, nil
	}

	// find end of front matter
	var fmEndFence string
	fmEndFenceStart := -1
	for _, fence := range closingFence {
		index := strings.Index(input[firstLineEnd:], "\n"+fence)
		if index >= 0 {
			fmEndFenceStart = index
			fmEndFence = fence
			break
		}
	}
	if fmEndFenceStart < 0 {
		return nil, "", fmt.Errorf("unterminated front matter")
	}
	fmEndFenceStart += firstLineEnd + 1 // add 1 to account for newline

	// extract and parse front matter
	frontMatter := input[firstLineEnd:fmEndFenceStart]
	fm, err := fmParser([]byte(frontMatter))
	if err != nil {
		return nil, "", err
	}

	// the rest is the body
	body := input[fmEndFenceStart+len(fmEndFence):]

	return fm, body, nil
}

func yamlFrontMatter(input []byte) (map[string]any, error) {
	m := make(map[string]any)
	err := yaml.Unmarshal(input, &m)
	return m, err
}

func tomlFrontMatter(input []byte) (map[string]any, error) {
	m := make(map[string]any)
	err := toml.Unmarshal(input, &m)
	return m, err
}

func jsonFrontMatter(input []byte) (map[string]any, error) {
	input = append([]byte{'{'}, input...)
	input = append(input, '}')
	m := make(map[string]any)
	err := json.Unmarshal(input, &m)
	return m, err
}

type parsedMarkdownDoc struct {
	Meta map[string]any `json:"meta,omitempty"`
	Body string         `json:"body,omitempty"`
}

type frontMatterType struct {
	FenceOpen  string
	FenceClose []string
	ParseFunc  func(input []byte) (map[string]any, error)
}

var supportedFrontMatterTypes = []frontMatterType{
	{
		FenceOpen:  "---",
		FenceClose: []string{"---", "..."},
		ParseFunc:  yamlFrontMatter,
	},
	{
		FenceOpen:  "+++",
		FenceClose: []string{"+++"},
		ParseFunc:  tomlFrontMatter,
	},
	{
		FenceOpen:  "{",
		FenceClose: []string{"}"},
		ParseFunc:  jsonFrontMatter,
	},
}