1package lexers
2
3import (
4 . "github.com/alecthomas/chroma/v2" // nolint
5)
6
7// Matcher token stub for docs, or
8// Named matcher: @name, or
9// Path matcher: /foo, or
10// Wildcard path matcher: *
11// nolint: gosec
12var caddyfileMatcherTokenRegexp = `(\[\<matcher\>\]|@[^\s]+|/[^\s]+|\*)`
13
14// Comment at start of line, or
15// Comment preceded by whitespace
16var caddyfileCommentRegexp = `(^|\s+)#.*\n`
17
18// caddyfileCommon are the rules common to both of the lexer variants
19func caddyfileCommonRules() Rules {
20 return Rules{
21 "site_block_common": {
22 Include("site_body"),
23 // Any other directive
24 {`[^\s#]+`, Keyword, Push("directive")},
25 Include("base"),
26 },
27 "site_body": {
28 // Import keyword
29 {`\b(import|invoke)\b( [^\s#]+)`, ByGroups(Keyword, Text), Push("subdirective")},
30 // Matcher definition
31 {`@[^\s]+(?=\s)`, NameDecorator, Push("matcher")},
32 // Matcher token stub for docs
33 {`\[\<matcher\>\]`, NameDecorator, Push("matcher")},
34 // These cannot have matchers but may have things that look like
35 // matchers in their arguments, so we just parse as a subdirective.
36 {`\b(try_files|tls|log|bind)\b`, Keyword, Push("subdirective")},
37 // These are special, they can nest more directives
38 {`\b(handle_errors|handle_path|handle_response|replace_status|handle|route)\b`, Keyword, Push("nested_directive")},
39 // uri directive has special syntax
40 {`\b(uri)\b`, Keyword, Push("uri_directive")},
41 },
42 "matcher": {
43 {`\{`, Punctuation, Push("block")},
44 // Not can be one-liner
45 {`not`, Keyword, Push("deep_not_matcher")},
46 // Heredoc for CEL expression
47 Include("heredoc"),
48 // Backtick for CEL expression
49 {"`", StringBacktick, Push("backticks")},
50 // Any other same-line matcher
51 {`[^\s#]+`, Keyword, Push("arguments")},
52 // Terminators
53 {`\s*\n`, Text, Pop(1)},
54 {`\}`, Punctuation, Pop(1)},
55 Include("base"),
56 },
57 "block": {
58 {`\}`, Punctuation, Pop(2)},
59 // Using double quotes doesn't stop at spaces
60 {`"`, StringDouble, Push("double_quotes")},
61 // Using backticks doesn't stop at spaces
62 {"`", StringBacktick, Push("backticks")},
63 // Not can be one-liner
64 {`not`, Keyword, Push("not_matcher")},
65 // Directives & matcher definitions
66 Include("site_body"),
67 // Any directive
68 {`[^\s#]+`, Keyword, Push("subdirective")},
69 Include("base"),
70 },
71 "nested_block": {
72 {`\}`, Punctuation, Pop(2)},
73 // Using double quotes doesn't stop at spaces
74 {`"`, StringDouble, Push("double_quotes")},
75 // Using backticks doesn't stop at spaces
76 {"`", StringBacktick, Push("backticks")},
77 // Not can be one-liner
78 {`not`, Keyword, Push("not_matcher")},
79 // Directives & matcher definitions
80 Include("site_body"),
81 // Any other subdirective
82 {`[^\s#]+`, Keyword, Push("directive")},
83 Include("base"),
84 },
85 "not_matcher": {
86 {`\}`, Punctuation, Pop(2)},
87 {`\{(?=\s)`, Punctuation, Push("block")},
88 {`[^\s#]+`, Keyword, Push("arguments")},
89 {`\s+`, Text, nil},
90 },
91 "deep_not_matcher": {
92 {`\}`, Punctuation, Pop(2)},
93 {`\{(?=\s)`, Punctuation, Push("block")},
94 {`[^\s#]+`, Keyword, Push("deep_subdirective")},
95 {`\s+`, Text, nil},
96 },
97 "directive": {
98 {`\{(?=\s)`, Punctuation, Push("block")},
99 {caddyfileMatcherTokenRegexp, NameDecorator, Push("arguments")},
100 {caddyfileCommentRegexp, CommentSingle, Pop(1)},
101 {`\s*\n`, Text, Pop(1)},
102 Include("base"),
103 },
104 "nested_directive": {
105 {`\{(?=\s)`, Punctuation, Push("nested_block")},
106 {caddyfileMatcherTokenRegexp, NameDecorator, Push("nested_arguments")},
107 {caddyfileCommentRegexp, CommentSingle, Pop(1)},
108 {`\s*\n`, Text, Pop(1)},
109 Include("base"),
110 },
111 "subdirective": {
112 {`\{(?=\s)`, Punctuation, Push("block")},
113 {caddyfileCommentRegexp, CommentSingle, Pop(1)},
114 {`\s*\n`, Text, Pop(1)},
115 Include("base"),
116 },
117 "arguments": {
118 {`\{(?=\s)`, Punctuation, Push("block")},
119 {caddyfileCommentRegexp, CommentSingle, Pop(2)},
120 {`\\\n`, Text, nil}, // Skip escaped newlines
121 {`\s*\n`, Text, Pop(2)},
122 Include("base"),
123 },
124 "nested_arguments": {
125 {`\{(?=\s)`, Punctuation, Push("nested_block")},
126 {caddyfileCommentRegexp, CommentSingle, Pop(2)},
127 {`\\\n`, Text, nil}, // Skip escaped newlines
128 {`\s*\n`, Text, Pop(2)},
129 Include("base"),
130 },
131 "deep_subdirective": {
132 {`\{(?=\s)`, Punctuation, Push("block")},
133 {caddyfileCommentRegexp, CommentSingle, Pop(3)},
134 {`\s*\n`, Text, Pop(3)},
135 Include("base"),
136 },
137 "uri_directive": {
138 {`\{(?=\s)`, Punctuation, Push("block")},
139 {caddyfileMatcherTokenRegexp, NameDecorator, nil},
140 {`(strip_prefix|strip_suffix|replace|path_regexp)`, NameConstant, Push("arguments")},
141 {caddyfileCommentRegexp, CommentSingle, Pop(1)},
142 {`\s*\n`, Text, Pop(1)},
143 Include("base"),
144 },
145 "double_quotes": {
146 Include("placeholder"),
147 {`\\"`, StringDouble, nil},
148 {`[^"]`, StringDouble, nil},
149 {`"`, StringDouble, Pop(1)},
150 },
151 "backticks": {
152 Include("placeholder"),
153 {"\\\\`", StringBacktick, nil},
154 {"[^`]", StringBacktick, nil},
155 {"`", StringBacktick, Pop(1)},
156 },
157 "optional": {
158 // Docs syntax for showing optional parts with [ ]
159 {`\[`, Punctuation, Push("optional")},
160 Include("name_constants"),
161 {`\|`, Punctuation, nil},
162 {`[^\[\]\|]+`, String, nil},
163 {`\]`, Punctuation, Pop(1)},
164 },
165 "heredoc": {
166 {`(<<([a-zA-Z0-9_-]+))(\n(.*|\n)*)(\s*)(\2)`, ByGroups(StringHeredoc, nil, String, String, String, StringHeredoc), nil},
167 },
168 "name_constants": {
169 {`\b(most_recently_modified|largest_size|smallest_size|first_exist|internal|disable_redirects|ignore_loaded_certs|disable_certs|private_ranges|first|last|before|after|on|off)\b(\||(?=\]|\s|$))`, ByGroups(NameConstant, Punctuation), nil},
170 },
171 "placeholder": {
172 // Placeholder with dots, colon for default value, brackets for args[0:]
173 {`\{[\w+.\[\]\:\$-]+\}`, StringEscape, nil},
174 // Handle opening brackets with no matching closing one
175 {`\{[^\}\s]*\b`, String, nil},
176 },
177 "base": {
178 {caddyfileCommentRegexp, CommentSingle, nil},
179 {`\[\<matcher\>\]`, NameDecorator, nil},
180 Include("name_constants"),
181 Include("heredoc"),
182 {`(https?://)?([a-z0-9.-]+)(:)([0-9]+)([^\s]*)`, ByGroups(Name, Name, Punctuation, NumberInteger, Name), nil},
183 {`\[`, Punctuation, Push("optional")},
184 {"`", StringBacktick, Push("backticks")},
185 {`"`, StringDouble, Push("double_quotes")},
186 Include("placeholder"),
187 {`[a-z-]+/[a-z-+]+`, String, nil},
188 {`[0-9]+([smhdk]|ns|us|ยตs|ms)?\b`, NumberInteger, nil},
189 {`[^\s\n#\{]+`, String, nil},
190 {`/[^\s#]*`, Name, nil},
191 {`\s+`, Text, nil},
192 },
193 }
194}
195
196// Caddyfile lexer.
197var Caddyfile = Register(MustNewLexer(
198 &Config{
199 Name: "Caddyfile",
200 Aliases: []string{"caddyfile", "caddy"},
201 Filenames: []string{"Caddyfile*"},
202 MimeTypes: []string{},
203 },
204 caddyfileRules,
205))
206
207func caddyfileRules() Rules {
208 return Rules{
209 "root": {
210 {caddyfileCommentRegexp, CommentSingle, nil},
211 // Global options block
212 {`^\s*(\{)\s*$`, ByGroups(Punctuation), Push("globals")},
213 // Top level import
214 {`(import)(\s+)([^\s]+)`, ByGroups(Keyword, Text, NameVariableMagic), nil},
215 // Snippets
216 {`(&?\([^\s#]+\))(\s*)(\{)`, ByGroups(NameVariableAnonymous, Text, Punctuation), Push("snippet")},
217 // Site label
218 {`[^#{(\s,]+`, GenericHeading, Push("label")},
219 // Site label with placeholder
220 {`\{[\w+.\[\]\:\$-]+\}`, StringEscape, Push("label")},
221 {`\s+`, Text, nil},
222 },
223 "globals": {
224 {`\}`, Punctuation, Pop(1)},
225 // Global options are parsed as subdirectives (no matcher)
226 {`[^\s#]+`, Keyword, Push("subdirective")},
227 Include("base"),
228 },
229 "snippet": {
230 {`\}`, Punctuation, Pop(1)},
231 Include("site_body"),
232 // Any other directive
233 {`[^\s#]+`, Keyword, Push("directive")},
234 Include("base"),
235 },
236 "label": {
237 // Allow multiple labels, comma separated, newlines after
238 // a comma means another label is coming
239 {`,\s*\n?`, Text, nil},
240 {` `, Text, nil},
241 // Site label with placeholder
242 Include("placeholder"),
243 // Site label
244 {`[^#{(\s,]+`, GenericHeading, nil},
245 // Comment after non-block label (hack because comments end in \n)
246 {`#.*\n`, CommentSingle, Push("site_block")},
247 // Note: if \n, we'll never pop out of the site_block, it's valid
248 {`\{(?=\s)|\n`, Punctuation, Push("site_block")},
249 },
250 "site_block": {
251 {`\}`, Punctuation, Pop(2)},
252 Include("site_block_common"),
253 },
254 }.Merge(caddyfileCommonRules())
255}
256
257// Caddyfile directive-only lexer.
258var CaddyfileDirectives = Register(MustNewLexer(
259 &Config{
260 Name: "Caddyfile Directives",
261 Aliases: []string{"caddyfile-directives", "caddyfile-d", "caddy-d"},
262 Filenames: []string{},
263 MimeTypes: []string{},
264 },
265 caddyfileDirectivesRules,
266))
267
268func caddyfileDirectivesRules() Rules {
269 return Rules{
270 // Same as "site_block" in Caddyfile
271 "root": {
272 Include("site_block_common"),
273 },
274 }.Merge(caddyfileCommonRules())
275}