1package parser
2
3import (
4 "bytes"
5 "path"
6 "path/filepath"
7)
8
9// isInclude parses {{...}}[...], that contains a path between the {{, the [...] syntax contains
10// an address to select which lines to include. It is treated as an opaque string and just given
11// to readInclude.
12func (p *Parser) isInclude(data []byte) (filename string, address []byte, consumed int) {
13 i := skipCharN(data, 0, ' ', 3) // start with up to 3 spaces
14 if len(data[i:]) < 3 {
15 return "", nil, 0
16 }
17 if data[i] != '{' || data[i+1] != '{' {
18 return "", nil, 0
19 }
20 start := i + 2
21
22 // find the end delimiter
23 i = skipUntilChar(data, i, '}')
24 if i+1 >= len(data) {
25 return "", nil, 0
26 }
27 end := i
28 i++
29 if data[i] != '}' {
30 return "", nil, 0
31 }
32 filename = string(data[start:end])
33
34 if i+1 < len(data) && data[i+1] == '[' { // potential address specification
35 start := i + 2
36
37 end = skipUntilChar(data, start, ']')
38 if end >= len(data) {
39 return "", nil, 0
40 }
41 address = data[start:end]
42 return filename, address, end + 1
43 }
44
45 return filename, address, i + 1
46}
47
48func (p *Parser) readInclude(from, file string, address []byte) []byte {
49 if p.Opts.ReadIncludeFn != nil {
50 return p.Opts.ReadIncludeFn(from, file, address)
51 }
52
53 return nil
54}
55
56// isCodeInclude parses <{{...}} which is similar to isInclude the returned bytes are, however wrapped in a code block.
57func (p *Parser) isCodeInclude(data []byte) (filename string, address []byte, consumed int) {
58 i := skipCharN(data, 0, ' ', 3) // start with up to 3 spaces
59 if len(data[i:]) < 3 {
60 return "", nil, 0
61 }
62 if data[i] != '<' {
63 return "", nil, 0
64 }
65 start := i
66
67 filename, address, consumed = p.isInclude(data[i+1:])
68 if consumed == 0 {
69 return "", nil, 0
70 }
71 return filename, address, start + consumed + 1
72}
73
74// readCodeInclude acts like include except the returned bytes are wrapped in a fenced code block.
75func (p *Parser) readCodeInclude(from, file string, address []byte) []byte {
76 data := p.readInclude(from, file, address)
77 if data == nil {
78 return nil
79 }
80 ext := path.Ext(file)
81 buf := &bytes.Buffer{}
82 buf.Write([]byte("```"))
83 if ext != "" { // starts with a dot
84 buf.WriteString(" " + ext[1:] + "\n")
85 } else {
86 buf.WriteByte('\n')
87 }
88 buf.Write(data)
89 buf.WriteString("```\n")
90 return buf.Bytes()
91}
92
93// incStack hold the current stack of chained includes. Each value is the containing
94// path of the file being parsed.
95type incStack struct {
96 stack []string
97}
98
99func newIncStack() *incStack {
100 return &incStack{stack: []string{}}
101}
102
103// Push updates i with new.
104func (i *incStack) Push(new string) {
105 if path.IsAbs(new) {
106 i.stack = append(i.stack, path.Dir(new))
107 return
108 }
109 last := ""
110 if len(i.stack) > 0 {
111 last = i.stack[len(i.stack)-1]
112 }
113 i.stack = append(i.stack, path.Dir(filepath.Join(last, new)))
114}
115
116// Pop pops the last value.
117func (i *incStack) Pop() {
118 if len(i.stack) == 0 {
119 return
120 }
121 i.stack = i.stack[:len(i.stack)-1]
122}
123
124func (i *incStack) Last() string {
125 if len(i.stack) == 0 {
126 return ""
127 }
128 return i.stack[len(i.stack)-1]
129}